Replace runtime Lua and jq with ucode

This commit is contained in:
ushan0v
2026-05-27 02:23:03 +03:00
parent a8d70d9ce3
commit 87960c9c6c
23 changed files with 5610 additions and 5935 deletions
+157 -16
View File
@@ -122,6 +122,154 @@ http_get() {
esac
}
install_json_helper_path() {
helper_path="$TMP_DIR/install-json.uc"
if [ ! -s "$helper_path" ]; then
cat > "$helper_path" <<'EOF'
#!/usr/bin/env ucode
let fs = require("fs");
function as_string(value) {
return value == null ? "" : "" + value;
}
function read_stdin() {
let input = fs.open("/dev/stdin", "r");
if (!input)
return "";
let data = input.read("all");
input.close();
return data == null ? "" : data;
}
function read_json_file(path) {
try {
return json(fs.readfile(path));
}
catch (e) {
return null;
}
}
function read_stdin_json() {
try {
return json(read_stdin());
}
catch (e) {
return null;
}
}
function starts_with(value, prefix) {
value = as_string(value);
prefix = as_string(prefix);
return substr(value, 0, length(prefix)) == prefix;
}
function ends_with(value, suffix) {
value = as_string(value);
suffix = as_string(suffix);
return length(value) >= length(suffix) && substr(value, length(value) - length(suffix)) == suffix;
}
function asset_matches(name, kind, ext) {
let suffix = "." + ext;
if (!ends_with(name, suffix))
return false;
if (kind == "backend")
return starts_with(name, "podkop-plus_") || starts_with(name, "podkop-plus-");
if (kind == "app")
return starts_with(name, "luci-app-podkop-plus_") || starts_with(name, "luci-app-podkop-plus-");
if (kind == "i18n")
return starts_with(name, "luci-i18n-podkop-plus-ru_") || starts_with(name, "luci-i18n-podkop-plus-ru-");
return false;
}
function release_has_asset(release, kind, ext) {
if (type(release) != "object" || type(release.assets) != "array")
return false;
for (let asset in release.assets)
if (type(asset) == "object" && asset_matches(asset.name, kind, ext))
return true;
return false;
}
function github_message() {
let value = read_stdin_json();
if (value == null)
exit(2);
if (type(value) == "object" && value.message != null)
print(as_string(value.message), "\n");
}
function release_tags(path, ext) {
for (let release in (read_json_file(path) || [])) {
if (type(release) != "object")
continue;
if (release.draft === true || release.prerelease === true)
continue;
if (!release_has_asset(release, "backend", ext) || !release_has_asset(release, "app", ext))
continue;
let tag = as_string(release.tag_name || "");
if (tag != "")
print(tag, "\n");
}
}
function release_by_tag(path, tag) {
for (let release in (read_json_file(path) || [])) {
if (type(release) == "object" && as_string(release.tag_name || "") == tag) {
print(sprintf("%J", release), "\n");
return;
}
}
}
function release_tag() {
let release = read_stdin_json();
if (type(release) == "object" && release.tag_name != null)
print(as_string(release.tag_name), "\n");
}
function release_asset_url(kind, ext) {
let release = read_stdin_json();
if (type(release) != "object" || type(release.assets) != "array")
return;
for (let asset in release.assets) {
if (type(asset) == "object" && asset_matches(asset.name, kind, ext)) {
print(as_string(asset.browser_download_url || ""), "\n");
return;
}
}
}
let mode = ARGV[0] || "";
if (mode == "github-message")
github_message();
else if (mode == "release-tags")
release_tags(ARGV[1], ARGV[2]);
else if (mode == "release-by-tag")
release_by_tag(ARGV[1], ARGV[2]);
else if (mode == "release-tag")
release_tag();
else if (mode == "release-asset-url")
release_asset_url(ARGV[1], ARGV[2]);
else
exit(1);
EOF
fi
printf '%s\n' "$helper_path"
}
install_json_ucode() {
ucode "$(install_json_helper_path)" "$@"
}
download_file_once() {
case "$FETCHER" in
wget)
@@ -487,9 +635,8 @@ fetch_github_releases_json() {
response="$(http_get "$url" 2>/dev/null || true)"
[ -n "$response" ] || fail "Failed to query GitHub releases metadata for ${owner}/${repo}"
printf '%s' "$response" | jq -e . >/dev/null 2>&1 || fail "GitHub returned an invalid response for ${owner}/${repo}"
message="$(printf '%s' "$response" | jq -r 'if type == "object" then (.message // empty) else empty end' 2>/dev/null)"
message="$(printf '%s' "$response" | install_json_ucode github-message 2>/dev/null)" ||
fail "GitHub returned an invalid response for ${owner}/${repo}"
case "$message" in
*"API rate limit"*|*"rate limit exceeded"*)
fail "GitHub API rate limit reached. Try again later."
@@ -511,13 +658,7 @@ select_latest_podkop_plus_release_json() {
releases_file="$(mktemp)"
cat > "$releases_file"
for tag in $(jq -r --arg ext "$asset_ext" '
.[]
| select(((.draft // false) | not) and ((.prerelease // false) | not))
| select(any(.assets[]?; ((.name | startswith("podkop-plus_")) or (.name | startswith("podkop-plus-"))) and (.name | endswith("." + $ext))))
| select(any(.assets[]?; ((.name | startswith("luci-app-podkop-plus_")) or (.name | startswith("luci-app-podkop-plus-"))) and (.name | endswith("." + $ext))))
| .tag_name // empty
' "$releases_file"); do
for tag in $(install_json_ucode release-tags "$releases_file" "$asset_ext"); do
podkop_plus_release_version_parts "$tag" >/dev/null 2>&1 || continue
if [ -z "$best_tag" ] || podkop_plus_release_version_lt "$best_tag" "$tag"; then
@@ -526,7 +667,7 @@ select_latest_podkop_plus_release_json() {
done
if [ -n "$best_tag" ]; then
jq -c --arg tag "$best_tag" '.[] | select(.tag_name == $tag)' "$releases_file" | sed -n '1p'
install_json_ucode release-by-tag "$releases_file" "$best_tag" | sed -n '1p'
fi
rm -f "$releases_file"
@@ -538,13 +679,13 @@ resolve_podkop_plus_release() {
[ "$PKG_IS_APK" -eq 1 ] && asset_ext="apk"
PODKOP_PLUS_RELEASE_JSON="$(fetch_github_releases_json "$REPO_OWNER" "$REPO_NAME" | select_latest_podkop_plus_release_json "$asset_ext")"
PODKOP_PLUS_RELEASE_TAG="$(printf '%s' "$PODKOP_PLUS_RELEASE_JSON" | jq -r '.tag_name // empty')"
PODKOP_PLUS_RELEASE_TAG="$(printf '%s' "$PODKOP_PLUS_RELEASE_JSON" | install_json_ucode release-tag 2>/dev/null)"
[ -n "$PODKOP_PLUS_RELEASE_TAG" ] || fail "Failed to detect the Podkop Plus release tag"
PODKOP_PLUS_BACKEND_URL="$(printf '%s' "$PODKOP_PLUS_RELEASE_JSON" | jq -r --arg ext "$asset_ext" '.assets[] | select(((.name | startswith("podkop-plus_")) or (.name | startswith("podkop-plus-"))) and (.name | endswith("." + $ext))) | .browser_download_url' | sed -n '1p')"
PODKOP_PLUS_BACKEND_URL="$(printf '%s' "$PODKOP_PLUS_RELEASE_JSON" | install_json_ucode release-asset-url backend "$asset_ext" 2>/dev/null | sed -n '1p')"
[ -n "$PODKOP_PLUS_BACKEND_URL" ] || fail "The Podkop Plus release does not contain a podkop-plus .$asset_ext package"
PODKOP_PLUS_APP_URL="$(printf '%s' "$PODKOP_PLUS_RELEASE_JSON" | jq -r --arg ext "$asset_ext" '.assets[] | select(((.name | startswith("luci-app-podkop-plus_")) or (.name | startswith("luci-app-podkop-plus-"))) and (.name | endswith("." + $ext))) | .browser_download_url' | sed -n '1p')"
PODKOP_PLUS_APP_URL="$(printf '%s' "$PODKOP_PLUS_RELEASE_JSON" | install_json_ucode release-asset-url app "$asset_ext" 2>/dev/null | sed -n '1p')"
[ -n "$PODKOP_PLUS_APP_URL" ] || fail "The Podkop Plus release does not contain a luci-app-podkop-plus .$asset_ext package"
PODKOP_PLUS_BACKEND_NAME="$(basename "$PODKOP_PLUS_BACKEND_URL")"
@@ -555,7 +696,7 @@ resolve_podkop_plus_release() {
PODKOP_PLUS_I18N_NAME=""
if [ "$PODKOP_PLUS_I18N_REQUESTED" -eq 1 ]; then
PODKOP_PLUS_I18N_URL="$(printf '%s' "$PODKOP_PLUS_RELEASE_JSON" | jq -r --arg ext "$asset_ext" '.assets[] | select(((.name | startswith("luci-i18n-podkop-plus-ru_")) or (.name | startswith("luci-i18n-podkop-plus-ru-"))) and (.name | endswith("." + $ext))) | .browser_download_url' | sed -n '1p')"
PODKOP_PLUS_I18N_URL="$(printf '%s' "$PODKOP_PLUS_RELEASE_JSON" | install_json_ucode release-asset-url i18n "$asset_ext" 2>/dev/null | sed -n '1p')"
[ -n "$PODKOP_PLUS_I18N_URL" ] || fail "The Podkop Plus release does not contain a luci-i18n-podkop-plus-ru .$asset_ext package"
PODKOP_PLUS_I18N_NAME="$(basename "$PODKOP_PLUS_I18N_URL")"
fi
@@ -740,7 +881,7 @@ main() {
deactivate_original_podkop_if_present
pkg_list_update || fail "Failed to update package lists"
ensure_bootstrap_tool "jq" "jq"
ensure_bootstrap_tool "ucode" "ucode"
resolve_podkop_plus_release
remove_conflicting_dns_proxy
+1 -1
View File
@@ -21,7 +21,7 @@ include $(INCLUDE_DIR)/package.mk
define Package/podkop-plus
SECTION:=net
CATEGORY:=Network
DEPENDS:=+sing-box +curl +jq +lua +luci-lib-jsonc +luci-lib-nixio +kmod-nft-tproxy +coreutils-base64 +coreutils-sort +bind-dig +nftables +kmod-nft-nat +kmod-nft-offload
DEPENDS:=+sing-box +curl +ucode +ucode-mod-fs +kmod-nft-tproxy +coreutils-base64 +coreutils-sort +bind-dig +nftables +kmod-nft-nat +kmod-nft-offload
CONFLICTS:=https-dns-proxy nextdns luci-app-passwall luci-app-passwall2
TITLE:=Podkop Plus backend service
URL:=https://github.com/ushan0v/podkop-plus
File diff suppressed because it is too large Load Diff
+28 -72
View File
@@ -428,14 +428,12 @@ validate_byedpi_strategy_json() {
local raw_opt="$1"
if check_byedpi_strategy "$raw_opt"; then
jq -cn '{valid: true, message: "", needle: "", needles: []}'
provider_status_ucode byedpi-validation 1 "" ""
return 0
fi
printf '%s\n' "$BYEDPI_VALIDATE_NEEDLES" | jq -R . | jq -sc \
--arg message "$BYEDPI_VALIDATE_ERROR" \
--arg needle "$BYEDPI_VALIDATE_NEEDLE" \
'{valid: false, message: $message, needle: $needle, needles: .}'
printf '%s\n' "$BYEDPI_VALIDATE_NEEDLES" |
provider_status_ucode byedpi-validation 0 "$BYEDPI_VALIDATE_ERROR" "$BYEDPI_VALIDATE_NEEDLE"
}
stop_byedpi_pidfile_process() {
@@ -668,13 +666,7 @@ byedpi_rule_outbound_present() {
outbound_tag="$(get_outbound_tag_by_section "$section")"
jq -e \
--arg tag "$outbound_tag" \
--arg address "$BYEDPI_LISTEN_ADDRESS" \
--argjson port "$port" \
'.outbounds[]? |
select(.type == "socks" and .tag == $tag and .server == $address and (.server_port // empty) == $port)' \
"$BYEDPI_SINGBOX_CONFIG_PATH" >/dev/null 2>&1
provider_status_ucode has-socks-outbound "$BYEDPI_SINGBOX_CONFIG_PATH" "$outbound_tag" "$BYEDPI_LISTEN_ADDRESS" "$port" >/dev/null 2>&1
}
byedpi_rule_route_rule_present() {
@@ -685,12 +677,7 @@ byedpi_rule_route_rule_present() {
outbound_tag="$(get_outbound_tag_by_section "$section")"
jq -e \
--arg inbound "$SB_TPROXY_INBOUND_TAG" \
--arg outbound "$outbound_tag" \
'.route.rules[]? |
select(.action == "route" and .inbound == $inbound and .outbound == $outbound)' \
"$BYEDPI_SINGBOX_CONFIG_PATH" >/dev/null 2>&1
provider_status_ucode has-route-rule "$BYEDPI_SINGBOX_CONFIG_PATH" "$SB_TPROXY_INBOUND_TAG" "$outbound_tag" >/dev/null 2>&1
}
_collect_byedpi_runtime_status_handler() {
@@ -816,51 +803,28 @@ get_byedpi_status_json() {
status_message="byedpi provider status is normal"
fi
jq -cn \
--arg version "$version" \
--arg provider_path "$BYEDPI_BIN" \
--arg listen_address "$BYEDPI_LISTEN_ADDRESS" \
--arg status_message "$status_message" \
--argjson installed "$installed" \
--argjson package_installed "$package_installed" \
--argjson provider_available "$provider_available" \
--argjson configured "$configured" \
--argjson enabled_rule_count "${enabled_rule_count:-0}" \
--argjson expected_process_count "${expected_process_count:-0}" \
--argjson running_process_count "${running_process_count:-0}" \
--argjson supervisor_process_count "${supervisor_process_count:-0}" \
--argjson restart_count "${restart_count:-0}" \
--argjson runtime_unstable "$runtime_unstable" \
--argjson standalone_service_enabled "$standalone_service_enabled" \
--argjson standalone_service_running "$standalone_service_running" \
--argjson port_base "$BYEDPI_PORT_BASE" \
--argjson outbounds_configured "$outbounds_configured" \
--argjson routes_configured "$routes_configured" \
--argjson ready "$ready" \
--argjson conflict "$conflict" \
'{
installed: $installed,
package_installed: $package_installed,
provider_available: $provider_available,
provider_path: $provider_path,
version: $version,
configured: $configured,
enabled_rule_count: $enabled_rule_count,
expected_process_count: $expected_process_count,
running_process_count: $running_process_count,
supervisor_process_count: $supervisor_process_count,
restart_count: $restart_count,
runtime_unstable: $runtime_unstable,
standalone_service_enabled: $standalone_service_enabled,
standalone_service_running: $standalone_service_running,
listen_address: $listen_address,
port_base: $port_base,
outbounds_configured: $outbounds_configured,
routes_configured: $routes_configured,
ready: $ready,
conflict: $conflict,
status_message: $status_message
}'
provider_status_ucode byedpi-status \
"$installed" \
"$package_installed" \
"$provider_available" \
"$BYEDPI_BIN" \
"$version" \
"$configured" \
"${enabled_rule_count:-0}" \
"${expected_process_count:-0}" \
"${running_process_count:-0}" \
"${supervisor_process_count:-0}" \
"${restart_count:-0}" \
"$runtime_unstable" \
"$standalone_service_enabled" \
"$standalone_service_running" \
"$BYEDPI_LISTEN_ADDRESS" \
"$BYEDPI_PORT_BASE" \
"$outbounds_configured" \
"$routes_configured" \
"$ready" \
"$conflict" \
"$status_message"
}
check_byedpi_runtime_json() {
@@ -874,13 +838,5 @@ check_byedpi_runtime_json() {
package_installed=1
fi
jq -cn \
--argjson byedpi_installed "$installed" \
--argjson byedpi_package_installed "$package_installed" \
--arg byedpi_provider_path "$BYEDPI_BIN" \
'{
byedpi_installed: $byedpi_installed,
byedpi_package_installed: $byedpi_package_installed,
byedpi_provider_path: $byedpi_provider_path
}'
provider_status_ucode byedpi-check "$installed" "$package_installed" "$BYEDPI_BIN"
}
-1
View File
@@ -19,7 +19,6 @@ TMP_SING_BOX_FOLDER="/tmp/sing-box"
TMP_RULESET_FOLDER="$TMP_SING_BOX_FOLDER/rulesets"
TMP_SUBSCRIPTION_FOLDER="$TMP_SING_BOX_FOLDER/subscriptions"
CLOUDFLARE_OCTETS="8.47 162.159 188.114" # Endpoints https://github.com/ampetelin/warp-endpoint-checker
JQ_REQUIRED_VERSION="1.7.1"
COREUTILS_BASE64_REQUIRED_VERSION="9.7"
RT_TABLE_NAME="podkopplus"
-14
View File
@@ -1,14 +0,0 @@
def extend_key_value(current_value; new_value):
if (current_value | type) == "array" then
if (new_value | type) == "array" then
current_value + new_value
else
current_value + [new_value]
end
else
if (new_value | type) == "array" then
[current_value] + new_value
else
[current_value, new_value]
end
end;
+9 -5
View File
@@ -425,16 +425,20 @@ download_subscription() {
return 1
}
json_utils_ucode() {
ucode "${PODKOP_LIB:-/usr/lib/podkop-plus}/json_utils.uc" "$@"
}
validate_subscription_file() {
local filepath="$1"
[ -s "$filepath" ] || return 1
jq -e '
type == "object" and
(.outbounds | type == "array") and
((.outbounds | length) > 0)
' "$filepath" > /dev/null 2>&1
json_utils_ucode validate-subscription "$filepath" >/dev/null 2>&1
}
provider_status_ucode() {
ucode "${PODKOP_LIB:-/usr/lib/podkop-plus}/provider_status.uc" "$@"
}
# Converts Windows-style line endings (CRLF) to Unix-style (LF)
+927
View File
@@ -0,0 +1,927 @@
#!/usr/bin/env ucode
let fs = require("fs");
function as_string(value) {
return value == null ? "" : "" + value;
}
function read_json_file(path) {
let data = fs.readfile(path);
if (data == null)
return null;
try {
return json(data);
}
catch (e) {
return null;
}
}
function read_stdin() {
let input = fs.open("/dev/stdin", "r");
if (!input)
return "";
let data = input.read("all");
input.close();
return data == null ? "" : data;
}
function read_stdin_json() {
let data = read_stdin();
try {
return json(data);
}
catch (e) {
return null;
}
}
function write_json(value) {
print(sprintf("%J", value), "\n");
}
function write_json_file(path, value) {
return fs.writefile(path, sprintf("%J", value) + "\n");
}
function str_contains(haystack, needle) {
return index(as_string(haystack), as_string(needle)) >= 0;
}
function str_startswith(value, prefix) {
value = as_string(value);
prefix = as_string(prefix);
return substr(value, 0, length(prefix)) == prefix;
}
function str_endswith(value, suffix) {
value = as_string(value);
suffix = as_string(suffix);
return length(value) >= length(suffix) && substr(value, length(value) - length(suffix)) == suffix;
}
function array_or_empty(value) {
return type(value) == "array" ? value : [];
}
function object_or_empty(value) {
return type(value) == "object" ? value : {};
}
function validate_subscription(path) {
let value = read_json_file(path);
return type(value) == "object" && type(value.outbounds) == "array" && length(value.outbounds) > 0;
}
function object_has_extra_keys(path) {
let value = read_json_file(path);
return type(value) == "object" && length(keys(value)) > 1;
}
function value_length(path) {
let value = read_json_file(path);
if (type(value) == "array" || type(value) == "object")
print(length(value), "\n");
else
print("0\n");
}
function response_success() {
let value = read_stdin_json();
return type(value) == "object" && value.success === true;
}
function valid_outbound() {
let value = read_stdin_json();
return type(value) == "object" && type(value.type) == "string";
}
function stdin_length() {
let value = read_stdin_json();
if (type(value) == "array" || type(value) == "object")
print(length(value), "\n");
else
print("0\n");
}
function array_item(index) {
let value = read_stdin_json();
index = int(index || 0);
if (type(value) == "array" && index >= 0 && index < length(value) && value[index] != null)
print(as_string(value[index]), "\n");
}
function object_get(key) {
let value = read_stdin_json();
if (type(value) == "object" && value[key] != null)
print(as_string(value[key]), "\n");
}
function object_get_default(key, fallback) {
let value = read_stdin_json();
if (type(value) == "object" && value[key] != null)
print(as_string(value[key]), "\n");
else
print(as_string(fallback), "\n");
}
function parse_json_or_null(value) {
try {
return json(value);
}
catch (e) {
return null;
}
}
function arg_bool(value) {
return value === true || value == "true" || value == "1" || value == 1;
}
function arg_number(value) {
value = as_string(value);
if (value == "" || match(value, /[^0-9-]/))
return 0;
return int(value);
}
function arg_optional_number(value) {
value = as_string(value);
if (value == "" || value == "null" || match(value, /[^0-9-]/))
return null;
return int(value);
}
function object_json(start) {
let result = {};
for (let i = start; i + 2 < length(ARGV); i += 3) {
let key = as_string(ARGV[i]);
let kind = as_string(ARGV[i + 1]);
let value = ARGV[i + 2];
if (kind == "n")
result[key] = arg_number(value);
else if (kind == "b")
result[key] = arg_bool(value);
else if (kind == "j")
result[key] = parse_json_or_null(value);
else if (kind == "a") {
let parsed = parse_json_or_null(value);
result[key] = parsed != null ? parsed : as_string(value);
}
else
result[key] = as_string(value);
}
write_json(result);
}
function subscription_ui_metadata() {
let title = as_string(ARGV[1]);
let web_page_url = as_string(ARGV[2]);
let support_url = as_string(ARGV[3]);
let announce = as_string(ARGV[4]);
let announce_url = as_string(ARGV[5]);
let file_name = as_string(ARGV[6]);
let has_traffic = arg_bool(ARGV[7]);
let upload = arg_optional_number(ARGV[8]);
let download = arg_optional_number(ARGV[9]);
let used = arg_number(ARGV[10]);
let total = arg_optional_number(ARGV[11]);
let remaining = arg_optional_number(ARGV[12]);
let is_unlimited = arg_bool(ARGV[13]);
let expire = arg_optional_number(ARGV[14]);
let refill_date = arg_optional_number(ARGV[15]);
let result = { version: 1 };
if (title != "")
result.title = title;
if (has_traffic) {
let traffic = {
used: used,
isUnlimited: is_unlimited
};
if (upload != null)
traffic.upload = upload;
if (download != null)
traffic.download = download;
if (total != null && total > 0) {
traffic.total = total;
traffic.remaining = remaining != null ? remaining : 0;
}
result.traffic = traffic;
}
if (expire != null && expire > 0)
result.expire = expire;
if (refill_date != null && refill_date > 0)
result.refillDate = refill_date;
if (web_page_url != "")
result.webPageUrl = web_page_url;
if (support_url != "")
result.supportUrl = support_url;
if (announce != "")
result.announce = announce;
if (announce_url != "")
result.announceUrl = announce_url;
if (file_name != "")
result.fileName = file_name;
if (length(keys(result)) > 1)
write_json(result);
}
function stdin_json() {
let value = read_stdin_json();
if (value == null)
exit(1);
write_json(value);
}
function file_json_valid(path) {
return read_json_file(path) != null;
}
function json_file_field(path, key, fallback) {
let value = read_json_file(path);
if (type(value) == "object" && value[key] != null)
print(as_string(value[key]), "\n");
else
print(as_string(fallback), "\n");
}
function github_release_tags(path) {
for (let release in array_or_empty(read_json_file(path))) {
if (type(release) != "object")
continue;
if (release.draft === true || release.prerelease === true)
continue;
let tag = as_string(release.tag_name || "");
if (tag != "")
print(tag, "\n");
}
}
function github_response_ok() {
let response = read_stdin_json();
if (response == null)
return false;
if (type(response) == "object") {
let message = as_string(response.message || "");
if (match(message, /API rate limit/) || match(message, /rate limit exceeded/) || message == "Not Found")
return false;
}
return true;
}
function release_by_tag(tag) {
for (let release in array_or_empty(read_stdin_json())) {
if (type(release) != "object")
continue;
if (release.draft === true || release.prerelease === true)
continue;
if (as_string(release.tag_name || "") == tag) {
write_json(release);
return;
}
}
}
function release_asset_name(prefix, ext) {
let release = object_or_empty(read_stdin_json());
for (let asset in array_or_empty(release.assets)) {
if (type(asset) != "object")
continue;
let name = as_string(asset.name || "");
if ((str_startswith(name, prefix + "_") || str_startswith(name, prefix + "-")) &&
str_endswith(name, "." + ext)) {
print(name, "\n");
return;
}
}
}
function release_asset_url(name) {
let release = object_or_empty(read_stdin_json());
for (let asset in array_or_empty(release.assets)) {
if (type(asset) == "object" && as_string(asset.name || "") == name) {
print(as_string(asset.browser_download_url || ""), "\n");
return;
}
}
}
function release_asset_name_by_suffix(suffix) {
let release = object_or_empty(read_stdin_json());
for (let asset in array_or_empty(release.assets)) {
let name = type(asset) == "object" ? as_string(asset.name || "") : "";
if (str_endswith(name, suffix)) {
print(name, "\n");
return;
}
}
}
function release_asset_url_by_suffix(suffix) {
let release = object_or_empty(read_stdin_json());
for (let asset in array_or_empty(release.assets)) {
if (type(asset) != "object")
continue;
let name = as_string(asset.name || "");
if (str_endswith(name, suffix)) {
print(as_string(asset.browser_download_url || ""), "\n");
return;
}
}
}
function byedpi_asset_matches(name, arch, ext) {
return (str_startswith(name, "byedpi_") || str_startswith(name, "byedpi-")) &&
str_endswith(name, "." + ext) &&
(str_contains(name, "_" + arch + "." + ext) || str_contains(name, "-" + arch + "." + ext));
}
function select_byedpi_asset_from_release(release, asset_ext, arch_candidates) {
for (let arch in split(as_string(arch_candidates), " ")) {
if (arch == "")
continue;
for (let asset in array_or_empty(release.assets)) {
if (type(asset) != "object")
continue;
let name = as_string(asset.name || "");
let url = as_string(asset.browser_download_url || "");
if (url != "" && byedpi_asset_matches(name, arch, asset_ext)) {
print(arch, "\t", name, "\t", url, "\n");
return true;
}
}
}
return false;
}
function byedpi_select_asset(series, asset_ext, arch_candidates) {
let releases = array_or_empty(read_stdin_json());
for (let pass = 0; pass < 2; pass++) {
if (pass == 0 && as_string(series) == "")
continue;
for (let release in releases) {
if (type(release) != "object")
continue;
if (release.draft === true || release.prerelease === true)
continue;
if (pass == 0) {
let tag = as_string(release.tag_name || "");
let name = as_string(release.name || "");
if (!str_contains(tag, series) && !str_contains(name, series))
continue;
}
if (select_byedpi_asset_from_release(release, asset_ext, arch_candidates))
return;
}
}
}
function sing_box_extended_release_tag() {
for (let release in array_or_empty(read_stdin_json())) {
if (type(release) != "object")
continue;
if (release.draft === true || release.prerelease === true)
continue;
let tag = as_string(release.tag_name || "");
let lowered = lc(tag);
if (tag != "" && !str_contains(lowered, "alpha") && !str_contains(lowered, "beta") && !str_contains(lowered, "rc")) {
print(tag, "\n");
return;
}
}
}
function system_info_cache_valid(path, podkop_version, luci_app_version, ttl, now) {
let cache = read_json_file(path);
if (type(cache) != "object")
return false;
now = arg_number(now);
ttl = arg_number(ttl);
let cached_at = arg_number(cache.generated_at || 0);
if (now > 0 && cached_at > 0 && ttl > 0 && now - cached_at >= ttl)
return false;
return cache.podkop_version == podkop_version && cache.luci_app_version == luci_app_version;
}
function system_info_json() {
write_json({
podkop_version: as_string(ARGV[1]),
podkop_latest_version: as_string(ARGV[2]),
luci_app_version: as_string(ARGV[3]),
sing_box_version: as_string(ARGV[4]),
sing_box_extended: arg_number(ARGV[5]),
zapret_version: as_string(ARGV[6]),
zapret_installed: arg_number(ARGV[7]),
byedpi_version: as_string(ARGV[8]),
byedpi_installed: arg_number(ARGV[9]),
openwrt_version: as_string(ARGV[10]),
device_model: as_string(ARGV[11]),
generated_at: arg_number(ARGV[12])
});
}
function nfqws_strategy_validation(valid, message, needle, needles) {
let result = [];
for (let item in split(as_string(needles), "\n")) {
if (item != "")
push(result, item);
}
write_json({
valid: arg_bool(valid),
message: as_string(message),
needle: as_string(needle),
needles: result
});
}
function url_encode(value) {
value = as_string(value);
for (let i = 0; i < length(value); i++) {
let c = substr(value, i, 1);
let code = ord(c);
if ((code >= 48 && code <= 57) ||
(code >= 65 && code <= 90) ||
(code >= 97 && code <= 122) ||
c == "-" || c == "_" || c == "." || c == "~")
print(c);
else
print(sprintf("%%%02X", code));
}
print("\n");
}
function job_running_is(path, expected) {
let value = read_json_file(path);
let running = type(value) == "object" && value.running === true;
return running == arg_bool(expected);
}
function updates_set_running_job_pid(path, pid) {
let value = object_or_empty(read_json_file(path));
if (value.running === true)
value.pid = as_string(pid);
write_json(value);
}
function updates_mark_stale_job_state(path) {
let value = object_or_empty(read_json_file(path));
if (value.running === true) {
value.success = false;
value.running = false;
value.message = "Component action job is stale or the worker process exited unexpectedly";
value.changed = 0;
value.status = "";
value.exit_code = null;
}
write_json(value);
}
function updates_finish_job_state(path, exit_code, updated_at) {
let value = read_json_file(path);
if (value == null)
exit(1);
value.running = false;
value.exit_code = arg_number(exit_code);
value.updated_at = arg_number(updated_at);
write_json(value);
}
function updates_fallback_job_state(component, action, message, exit_code, updated_at) {
write_json({
success: false,
running: false,
component: as_string(component),
action: as_string(action),
message: as_string(message),
current_version: "",
latest_version: "",
changed: 0,
status: "",
exit_code: arg_number(exit_code),
updated_at: arg_number(updated_at)
});
}
function sing_box_service_pid() {
let value = read_stdin_json();
let service = type(value) == "object" ? value["sing-box"] : null;
let instances = service && type(service.instances) == "object" ? service.instances : {};
for (let _, instance in instances) {
if (type(instance) == "object" && instance.running === true && int(instance.pid || 0) > 0) {
print(instance.pid, "\n");
return;
}
}
}
function array_append_string(path, value) {
let result = array_or_empty(read_json_file(path));
push(result, as_string(value));
write_json(result);
}
function arrays_concat(first_path, second_path) {
let result = array_or_empty(read_json_file(first_path));
for (let value in array_or_empty(read_json_file(second_path)))
push(result, value);
write_json(result);
}
function object_set_string(path, key, value) {
let result = object_or_empty(read_json_file(path));
result[as_string(key)] = as_string(value);
write_json(result);
}
function objects_merge(first_path, second_path) {
let result = object_or_empty(read_json_file(first_path));
for (let key, value in object_or_empty(read_json_file(second_path)))
result[key] = value;
write_json(result);
}
function normalized_country_list() {
let result = [];
for (let value in array_or_empty(read_stdin_json())) {
value = uc(as_string(value));
if (length(value) == 2)
push(result, value);
}
write_json(result);
}
function contains(values, needle) {
for (let value in values) {
if (value == needle)
return true;
}
return false;
}
function urltest_filter(mode, tags_path, names_path, countries_path, names_filter_path, regex_tags_path, countries_filter_path) {
let tags = array_or_empty(read_json_file(tags_path));
let names = object_or_empty(read_json_file(names_path));
let countries = object_or_empty(read_json_file(countries_path));
let name_filter = array_or_empty(read_json_file(names_filter_path));
let regex_tags = array_or_empty(read_json_file(regex_tags_path));
let country_filter = array_or_empty(read_json_file(countries_filter_path));
let result = [];
for (let tag in tags) {
let name = as_string(names[tag] || tag);
let country = uc(as_string(countries[tag] || ""));
let matched = contains(name_filter, name) ||
contains(name_filter, tag) ||
(country != "" && contains(country_filter, country)) ||
contains(regex_tags, tag);
if ((mode == "include" && matched) || (mode == "exclude" && !matched))
push(result, tag);
}
write_json(result);
}
function section_countries(path) {
let cache = object_or_empty(read_json_file(path));
write_json(object_or_empty(cache.outboundMetadata && cache.outboundMetadata.countries));
}
function cached_countries_for_servers(servers_path, cache_path) {
let servers = object_or_empty(read_json_file(servers_path));
let cache = object_or_empty(read_json_file(cache_path));
let result = {};
for (let tag, _ in servers) {
let country = as_string(cache[tag] || "");
if (country != "")
result[tag] = country;
}
write_json(result);
}
function missing_servers_tsv(servers_path, cache_path) {
let servers = object_or_empty(read_json_file(servers_path));
let cache = object_or_empty(read_json_file(cache_path));
for (let tag, server in servers) {
server = as_string(server);
if (server != "" && as_string(cache[tag] || "") == "")
print(tag, "\t", server, "\n");
}
}
function body_error(path) {
let body = read_json_file(path);
let result = "";
if (type(body) == "object")
result = as_string((body.error && body.error.code) || body.code || body.error || "");
print(result, "\n");
}
function ip_country_tsv(path) {
for (let item in array_or_empty(read_json_file(path))) {
if (type(item) != "object")
continue;
let ip = as_string(item.ip || "");
let country = as_string(item.country || "");
if (ip != "" && country != "")
print(ip, "\t", country, "\n");
}
}
function tsv_to_object(path) {
let result = {};
let data = fs.readfile(path);
if (data != null) {
for (let line in split(data, "\n")) {
if (line == "")
continue;
let parts = split(line, "\t");
if (length(parts) >= 2)
result[parts[0]] = parts[1];
}
}
write_json(result);
}
function tsv_second_column_array(path) {
let seen = {};
let result = [];
let data = fs.readfile(path);
if (data != null) {
for (let line in split(data, "\n")) {
if (line == "")
continue;
let parts = split(line, "\t");
if (length(parts) >= 2 && parts[1] != "" && !seen[parts[1]]) {
seen[parts[1]] = true;
push(result, parts[1]);
}
}
}
sort(result, function(first, second) {
return first == second ? 0 : (first < second ? -1 : 1);
});
write_json(result);
}
function array_slice_file(path, start, end) {
write_json(slice(array_or_empty(read_json_file(path)), int(start || 0), int(end || 0)));
}
function object_nonempty_stdin() {
let value = read_stdin_json();
return (type(value) == "array" || type(value) == "object") && length(value) > 0;
}
function resolved_countries_from_tsv(resolved_path, ip_country_path) {
let ip_country = object_or_empty(read_json_file(ip_country_path));
let result = {};
let data = fs.readfile(resolved_path);
if (data != null) {
for (let line in split(data, "\n")) {
if (line == "")
continue;
let parts = split(line, "\t");
if (length(parts) < 2)
continue;
let country = as_string(ip_country[parts[1]] || "");
if (country != "")
result[parts[0]] = country;
}
}
write_json(result);
}
function outbound_server_by_tag(tag) {
let config = object_or_empty(read_stdin_json());
for (let outbound in array_or_empty(config.outbounds)) {
if (type(outbound) == "object" && outbound.tag == tag) {
print(as_string(outbound.server || ""), "\n");
return;
}
}
}
let masked_sing_box_keys = {
uuid: true,
server: true,
server_name: true,
password: true,
public_key: true,
short_id: true,
fingerprint: true,
server_port: true
};
function mask_sing_box_value(value) {
if (type(value) == "array") {
let result = [];
for (let item in value)
push(result, mask_sing_box_value(item));
return result;
}
if (type(value) == "object") {
let result = {};
for (let key, item in value)
result[key] = masked_sing_box_keys[key] ? "MASKED" : mask_sing_box_value(item);
return result;
}
return value;
}
function mask_sing_box_config(path) {
write_json(mask_sing_box_value(read_json_file(path)));
}
function prepare_check_proxy_config(input_path, output_path, cache_path) {
let config = object_or_empty(read_json_file(input_path));
config.inbounds = [];
config.services = [];
if (type(config.experimental) == "object") {
if (type(config.experimental.cache_file) == "object")
config.experimental.cache_file.path = as_string(cache_path);
delete config.experimental.clash_api;
}
if (!write_json_file(output_path, config))
exit(1);
}
function check_proxy_outbound_tag(config_path, domain) {
let config = object_or_empty(read_json_file(config_path));
for (let rule in array_or_empty(config.route && config.route.rules)) {
if (type(rule) != "object")
continue;
if (contains(array_or_empty(rule.domain), domain)) {
print(as_string(rule.outbound || ""), "\n");
return;
}
}
}
function dns_route_rule_exists(service_tag, tag) {
let config = object_or_empty(read_stdin_json());
for (let rule in array_or_empty(config.dns && config.dns.rules)) {
if (type(rule) == "object" && rule[service_tag] == tag)
return true;
}
return false;
}
function route_rule_has_resolve_matchers(service_tag, tag) {
let config = object_or_empty(read_stdin_json());
for (let rule in array_or_empty(config.route && config.route.rules)) {
if (type(rule) != "object" || rule[service_tag] != tag)
continue;
if (rule.domain != null || rule.domain_suffix != null || rule.domain_keyword != null ||
rule.domain_regex != null || rule.rule_set != null)
return true;
}
return false;
}
let mode = ARGV[0] || "";
if (mode == "validate-subscription")
exit(validate_subscription(ARGV[1]) ? 0 : 1);
else if (mode == "object-has-extra-keys")
exit(object_has_extra_keys(ARGV[1]) ? 0 : 1);
else if (mode == "length")
value_length(ARGV[1]);
else if (mode == "response-success")
exit(response_success() ? 0 : 1);
else if (mode == "valid-outbound")
exit(valid_outbound() ? 0 : 1);
else if (mode == "stdin-length")
stdin_length();
else if (mode == "array-item")
array_item(ARGV[1]);
else if (mode == "object-get")
object_get(ARGV[1]);
else if (mode == "object-get-default")
object_get_default(ARGV[1], ARGV[2]);
else if (mode == "object-json")
object_json(1);
else if (mode == "subscription-ui-metadata")
subscription_ui_metadata();
else if (mode == "stdin-json")
stdin_json();
else if (mode == "file-json-valid")
exit(file_json_valid(ARGV[1]) ? 0 : 1);
else if (mode == "json-file-field")
json_file_field(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "github-release-tags")
github_release_tags(ARGV[1]);
else if (mode == "github-response-ok")
exit(github_response_ok() ? 0 : 1);
else if (mode == "release-by-tag")
release_by_tag(ARGV[1]);
else if (mode == "release-asset-name")
release_asset_name(ARGV[1], ARGV[2]);
else if (mode == "release-asset-url")
release_asset_url(ARGV[1]);
else if (mode == "release-asset-name-by-suffix")
release_asset_name_by_suffix(ARGV[1]);
else if (mode == "release-asset-url-by-suffix")
release_asset_url_by_suffix(ARGV[1]);
else if (mode == "byedpi-select-asset")
byedpi_select_asset(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "sing-box-extended-release-tag")
sing_box_extended_release_tag();
else if (mode == "system-info-cache-valid")
exit(system_info_cache_valid(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5]) ? 0 : 1);
else if (mode == "system-info-json")
system_info_json();
else if (mode == "nfqws-strategy-validation")
nfqws_strategy_validation(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
else if (mode == "url-encode")
url_encode(ARGV[1]);
else if (mode == "job-running-is")
exit(job_running_is(ARGV[1], ARGV[2]) ? 0 : 1);
else if (mode == "updates-set-running-job-pid")
updates_set_running_job_pid(ARGV[1], ARGV[2]);
else if (mode == "updates-mark-stale-job-state")
updates_mark_stale_job_state(ARGV[1]);
else if (mode == "updates-finish-job-state")
updates_finish_job_state(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "updates-fallback-job-state")
updates_fallback_job_state(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5]);
else if (mode == "sing-box-service-pid")
sing_box_service_pid();
else if (mode == "array-append-string")
array_append_string(ARGV[1], ARGV[2]);
else if (mode == "arrays-concat")
arrays_concat(ARGV[1], ARGV[2]);
else if (mode == "object-set-string")
object_set_string(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "objects-merge")
objects_merge(ARGV[1], ARGV[2]);
else if (mode == "normalized-country-list")
normalized_country_list();
else if (mode == "urltest-filter")
urltest_filter(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], ARGV[6], ARGV[7]);
else if (mode == "section-countries")
section_countries(ARGV[1]);
else if (mode == "cached-countries-for-servers")
cached_countries_for_servers(ARGV[1], ARGV[2]);
else if (mode == "missing-servers-tsv")
missing_servers_tsv(ARGV[1], ARGV[2]);
else if (mode == "body-error")
body_error(ARGV[1]);
else if (mode == "ip-country-tsv")
ip_country_tsv(ARGV[1]);
else if (mode == "tsv-to-object")
tsv_to_object(ARGV[1]);
else if (mode == "tsv-second-column-array")
tsv_second_column_array(ARGV[1]);
else if (mode == "array-slice-file")
array_slice_file(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "object-nonempty")
exit(object_nonempty_stdin() ? 0 : 1);
else if (mode == "resolved-countries-from-tsv")
resolved_countries_from_tsv(ARGV[1], ARGV[2]);
else if (mode == "outbound-server-by-tag")
outbound_server_by_tag(ARGV[1]);
else if (mode == "mask-sing-box-config")
mask_sing_box_config(ARGV[1]);
else if (mode == "prepare-check-proxy-config")
prepare_check_proxy_config(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "check-proxy-outbound-tag")
check_proxy_outbound_tag(ARGV[1], ARGV[2]);
else if (mode == "dns-route-rule-exists")
exit(dns_route_rule_exists(ARGV[1], ARGV[2]) ? 0 : 1);
else if (mode == "route-rule-has-resolve-matchers")
exit(route_rule_has_resolve_matchers(ARGV[1], ARGV[2]) ? 0 : 1);
else {
warn("Usage: json_utils.uc <operation> ...\n");
exit(1);
}
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env ucode
let fs = require("fs");
function as_string(value) {
return value == null ? "" : "" + value;
}
function int_arg(value) {
value = as_string(value);
return value == "" ? 0 : int(value, 10);
}
function bool_arg(value) {
value = as_string(value);
return value == "1" || value == "true";
}
function read_stdin() {
let input = fs.open("/dev/stdin", "r");
if (!input)
return "";
let data = input.read("all");
input.close();
return data == null ? "" : data;
}
function read_json_file(path) {
let data = fs.readfile(path);
if (data == null)
return null;
try {
return json(data);
}
catch (e) {
return null;
}
}
function array_or_empty(value) {
return type(value) == "array" ? value : [];
}
function write_json(value) {
print(sprintf("%J", value), "\n");
}
function byedpi_validation_result(valid, message, needle) {
let needles = [];
if (!bool_arg(valid)) {
for (let line in split(read_stdin(), "\n")) {
if (line != "")
push(needles, line);
}
}
write_json({
valid: bool_arg(valid),
message: as_string(message),
needle: as_string(needle),
needles
});
}
function has_socks_outbound(config_path, tag, address, port) {
let config = read_json_file(config_path);
port = int_arg(port);
for (let outbound in array_or_empty(config && config.outbounds)) {
if (type(outbound) == "object" &&
outbound.type == "socks" &&
outbound.tag == tag &&
outbound.server == address &&
int(outbound.server_port || 0) == port)
return true;
}
return false;
}
function has_direct_mark_outbound(config_path, tag, routing_mark) {
let config = read_json_file(config_path);
routing_mark = int_arg(routing_mark);
for (let outbound in array_or_empty(config && config.outbounds)) {
if (type(outbound) == "object" &&
outbound.type == "direct" &&
outbound.tag == tag &&
int(outbound.routing_mark || 0) == routing_mark)
return true;
}
return false;
}
function has_route_rule(config_path, inbound, outbound_tag) {
let config = read_json_file(config_path);
for (let rule in array_or_empty(config && config.route && config.route.rules)) {
if (type(rule) == "object" &&
rule.action == "route" &&
rule.inbound == inbound &&
rule.outbound == outbound_tag)
return true;
}
return false;
}
function byedpi_status(args) {
write_json({
installed: bool_arg(args[0]),
package_installed: bool_arg(args[1]),
provider_available: bool_arg(args[2]),
provider_path: as_string(args[3]),
version: as_string(args[4]),
configured: bool_arg(args[5]),
enabled_rule_count: int_arg(args[6]),
expected_process_count: int_arg(args[7]),
running_process_count: int_arg(args[8]),
supervisor_process_count: int_arg(args[9]),
restart_count: int_arg(args[10]),
runtime_unstable: bool_arg(args[11]),
standalone_service_enabled: bool_arg(args[12]),
standalone_service_running: bool_arg(args[13]),
listen_address: as_string(args[14]),
port_base: int_arg(args[15]),
outbounds_configured: bool_arg(args[16]),
routes_configured: bool_arg(args[17]),
ready: bool_arg(args[18]),
conflict: bool_arg(args[19]),
status_message: as_string(args[20])
});
}
function byedpi_check(installed, package_installed, provider_path) {
write_json({
byedpi_installed: bool_arg(installed),
byedpi_package_installed: bool_arg(package_installed),
byedpi_provider_path: as_string(provider_path)
});
}
function zapret_status(args) {
write_json({
installed: bool_arg(args[0]),
package_installed: bool_arg(args[1]),
provider_available: bool_arg(args[2]),
provider_path: as_string(args[3]),
files_available: bool_arg(args[4]),
ipset_available: bool_arg(args[5]),
version: as_string(args[6]),
configured: bool_arg(args[7]),
enabled_rule_count: int_arg(args[8]),
expected_process_count: int_arg(args[9]),
running_process_count: int_arg(args[10]),
supervisor_process_count: int_arg(args[11]),
standalone_service_enabled: bool_arg(args[12]),
standalone_service_running: bool_arg(args[13]),
standalone_config_present: bool_arg(args[14]),
standalone_conflict: bool_arg(args[15]),
luci_app_installed: bool_arg(args[16]),
queue_base: int_arg(args[17]),
queue_range_end: int_arg(args[18]),
queue_overlap: bool_arg(args[19]),
legacy_runtime_present: bool_arg(args[20]),
ready: bool_arg(args[21]),
conflict: bool_arg(args[22]),
outbounds_configured: bool_arg(args[23]),
routes_configured: bool_arg(args[24]),
status_message: as_string(args[25])
});
}
function zapret_check(installed, package_installed, provider_path) {
write_json({
zapret_installed: bool_arg(installed),
zapret_package_installed: bool_arg(package_installed),
zapret_provider_path: as_string(provider_path)
});
}
let mode = ARGV[0] || "";
if (mode == "byedpi-validation")
byedpi_validation_result(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "has-socks-outbound")
exit(has_socks_outbound(ARGV[1], ARGV[2], ARGV[3], ARGV[4]) ? 0 : 1);
else if (mode == "has-direct-mark-outbound")
exit(has_direct_mark_outbound(ARGV[1], ARGV[2], ARGV[3]) ? 0 : 1);
else if (mode == "has-route-rule")
exit(has_route_rule(ARGV[1], ARGV[2], ARGV[3]) ? 0 : 1);
else if (mode == "byedpi-status")
byedpi_status(slice(ARGV, 1));
else if (mode == "byedpi-check")
byedpi_check(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "zapret-status")
zapret_status(slice(ARGV, 1));
else if (mode == "zapret-check")
zapret_check(ARGV[1], ARGV[2], ARGV[3]);
else {
warn("Usage: provider_status.uc <operation> ...\n");
exit(1);
}
+9 -11
View File
@@ -20,7 +20,7 @@ create_source_rule_set() {
return 3
fi
jq -n '{version: 3, rules: []}' > "$ruleset_filepath"
ucode "${PODKOP_LIB:-/usr/lib/podkop-plus}/rulesets.uc" create-source "$ruleset_filepath"
}
#######################################
@@ -38,17 +38,15 @@ patch_source_ruleset_rules() {
local key="$2"
local value="$3"
local tmpfile=$(mktemp)
local tmpfile
tmpfile="$(mktemp)" || return 1
jq --arg key "$key" --argjson value "$value" \
'( .rules | map(has($key)) | index(true) ) as $idx |
if $idx != null then
.rules[$idx][$key] = (.rules[$idx][$key] + $value | unique)
else
.rules += [{ ($key): $value }]
end' "$filepath" > "$tmpfile"
cp "$filepath" "$tmpfile" || {
rm -f "$tmpfile"
return 1
}
if [ $? -ne 0 ]; then
if ! ucode "${PODKOP_LIB:-/usr/lib/podkop-plus}/rulesets.uc" patch-source "$tmpfile" "$key" "$value"; then
rm -f "$tmpfile"
return 1
fi
@@ -159,5 +157,5 @@ extract_ip_cidr_from_json_ruleset_to_file() {
local output_file="$2"
log "Extracting ip_cidr entries from $json_file to $output_file" "debug"
jq -r '.rules[]? | select(has("ip_cidr")) | .ip_cidr[]?' "$json_file" > "$output_file"
ucode "${PODKOP_LIB:-/usr/lib/podkop-plus}/rulesets.uc" extract-ip-cidr "$json_file" "$output_file"
}
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env ucode
let fs = require("fs");
function as_string(value) {
return value == null ? "" : "" + value;
}
function json_decode_text(text) {
try {
return json(as_string(text));
}
catch (e) {
return null;
}
}
function read_json_file(path) {
let data = fs.readfile(path);
return data == null ? null : json_decode_text(data);
}
function write_json_file(path, value) {
return fs.writefile(path, sprintf("%J", value) + "\n");
}
function array_or_empty(value) {
return type(value) == "array" ? value : [];
}
function object_or_empty(value) {
return type(value) == "object" ? value : {};
}
function sort_values(values) {
sort(values, function(first, second) {
first = sprintf("%J", first);
second = sprintf("%J", second);
return first == second ? 0 : (first < second ? -1 : 1);
});
return values;
}
function unique_values(values) {
values = sort_values(values);
let result = [];
let previous = null;
let has_previous = false;
for (let value in values) {
let encoded = sprintf("%J", value);
if (!has_previous || encoded != previous) {
push(result, value);
previous = encoded;
has_previous = true;
}
}
return result;
}
function create_source(path) {
if (!write_json_file(path, { version: 3, rules: [] }))
exit(1);
}
function patch_source(path, key, value_text) {
let ruleset = object_or_empty(read_json_file(path));
let values = array_or_empty(json_decode_text(value_text));
if (type(ruleset.rules) != "array")
ruleset.rules = [];
let found = false;
for (let rule in ruleset.rules) {
if (type(rule) == "object" && rule[key] != null) {
let merged = [];
for (let item in array_or_empty(rule[key]))
push(merged, item);
for (let item in values)
push(merged, item);
rule[key] = unique_values(merged);
found = true;
break;
}
}
if (!found) {
let rule = {};
rule[key] = values;
push(ruleset.rules, rule);
}
if (!write_json_file(path, ruleset))
exit(1);
}
function extract_ip_cidr(json_path, output_path) {
let ruleset = object_or_empty(read_json_file(json_path));
let lines = [];
for (let rule in array_or_empty(ruleset.rules)) {
if (type(rule) != "object")
continue;
for (let ip in array_or_empty(rule.ip_cidr))
push(lines, as_string(ip));
}
if (!fs.writefile(output_path, length(lines) > 0 ? join("\n", lines) + "\n" : ""))
exit(1);
}
function value_has_domain_matchers(value) {
if (type(value) == "array") {
for (let item in value) {
if (value_has_domain_matchers(item))
return true;
}
return false;
}
if (type(value) != "object")
return false;
for (let key, item in value) {
if (key == "domain" || key == "domain_suffix" || key == "domain_keyword" || key == "domain_regex") {
if (type(item) == "array" && length(item) > 0)
return true;
if (type(item) == "string" && item != "")
return true;
}
if (value_has_domain_matchers(item))
return true;
}
return false;
}
function has_domain_matchers(path) {
return value_has_domain_matchers(read_json_file(path));
}
let mode = ARGV[0] || "";
if (mode == "create-source")
create_source(ARGV[1]);
else if (mode == "patch-source")
patch_source(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "extract-ip-cidr")
extract_ip_cidr(ARGV[1], ARGV[2]);
else if (mode == "has-domain-matchers")
exit(has_domain_matchers(ARGV[1]) ? 0 : 1);
else {
warn("Usage: rulesets.uc <create-source|patch-source|extract-ip-cidr> ...\n");
exit(1);
}
+177 -322
View File
@@ -1,7 +1,98 @@
PODKOP_LIB="/usr/lib/podkop-plus"
PODKOP_LIB="${PODKOP_LIB:-/usr/lib/podkop-plus}"
. "$PODKOP_LIB/helpers.sh"
. "$PODKOP_LIB/sing_box_config_manager.sh"
sing_box_cf_ucode() {
ucode "${PODKOP_LIB:-/usr/lib/podkop-plus}/sing_box_config_facade.uc" "$@"
}
sing_box_cf_ucode_input() {
local operation="$1"
local input="$2"
shift 2
printf '%s' "$input" |
ucode "${PODKOP_LIB:-/usr/lib/podkop-plus}/sing_box_config_facade.uc" "$operation" "$@"
}
sing_box_cf_tags_json_object() {
printf '{"tags":%s}' "${1:-[]}"
}
sing_box_cf_tags_json_count() {
sing_box_cf_ucode_input prepared-length "$(sing_box_cf_tags_json_object "$1")" tags 2>/dev/null
}
sing_box_cf_tags_json_csv() {
sing_box_cf_ucode_input prepared-tags-csv "$(sing_box_cf_tags_json_object "$1")" 2>/dev/null
}
sing_box_cf_outbounds_json_object() {
printf '{"outbounds":%s}' "${1:-[]}"
}
sing_box_cf_outbounds_json_count() {
sing_box_cf_ucode_input prepared-length "$(sing_box_cf_outbounds_json_object "$1")" outbounds 2>/dev/null
}
sing_box_cf_json_arrays_concat() {
local first_json="$1"
local second_json="$2"
local first_tmp second_tmp result status
first_tmp="$(mktemp)" || return 1
second_tmp="$(mktemp)" || {
rm -f "$first_tmp"
return 1
}
printf '%s' "$first_json" > "$first_tmp" || {
rm -f "$first_tmp" "$second_tmp"
return 1
}
printf '%s' "$second_json" > "$second_tmp" || {
rm -f "$first_tmp" "$second_tmp"
return 1
}
result="$(ucode "$PODKOP_LIB/json_utils.uc" arrays-concat "$first_tmp" "$second_tmp" 2>/dev/null)"
status=$?
rm -f "$first_tmp" "$second_tmp"
[ "$status" -eq 0 ] || return 1
[ -n "$result" ] || return 1
printf '%s\n' "$result"
}
sing_box_cf_json_objects_merge() {
local first_json="$1"
local second_json="$2"
local first_tmp second_tmp result status
first_tmp="$(mktemp)" || return 1
second_tmp="$(mktemp)" || {
rm -f "$first_tmp"
return 1
}
printf '%s' "$first_json" > "$first_tmp" || {
rm -f "$first_tmp" "$second_tmp"
return 1
}
printf '%s' "$second_json" > "$second_tmp" || {
rm -f "$first_tmp" "$second_tmp"
return 1
}
result="$(ucode "$PODKOP_LIB/json_utils.uc" objects-merge "$first_tmp" "$second_tmp" 2>/dev/null)"
status=$?
rm -f "$first_tmp" "$second_tmp"
[ "$status" -eq 0 ] || return 1
[ -n "$result" ] || return 1
printf '%s\n' "$result"
}
sing_box_cf_add_dns_server() {
local config="$1"
local type="$2"
@@ -362,14 +453,7 @@ sing_box_cf_add_single_key_reject_rule() {
sing_box_cf_subscription_candidate_outbounds() {
local subscription_json_path="$1"
jq -c '[.outbounds[]? | select(
type == "object" and
.type != "selector" and
.type != "urltest" and
.type != "direct" and
.type != "dns" and
.type != "block"
)]' "$subscription_json_path" 2>/dev/null
sing_box_cf_ucode candidate-outbounds "$subscription_json_path" 2>/dev/null
}
sing_box_cf_log_subscription_skips() {
@@ -377,19 +461,13 @@ sing_box_cf_log_subscription_skips() {
local context="$2"
local skipped_count reason_summary
skipped_count="$(printf '%s' "$prepared_json" | jq -r '.skipped // 0' 2>/dev/null)"
skipped_count="$(sing_box_cf_ucode_input skip-count "$prepared_json" 2>/dev/null)"
case "$skipped_count" in
'' | *[!0-9]*) return 0 ;;
esac
[ "$skipped_count" -gt 0 ] || return 0
reason_summary="$(printf '%s' "$prepared_json" | jq -r '
(.skipped_reason_counts // {})
| to_entries
| sort_by([-.value, .key])
| map("\(.value)x \(.key)")
| join("; ")
' 2>/dev/null)"
reason_summary="$(sing_box_cf_ucode_input skip-summary "$prepared_json" 2>/dev/null)"
if [ -n "$reason_summary" ]; then
log "Skipped $skipped_count subscription outbounds $context: $reason_summary" "warn"
@@ -400,43 +478,21 @@ sing_box_cf_log_subscription_skips() {
sing_box_cf_subscription_plugin_supports() {
local outbounds_json="$1"
local plugins_tmp records_tmp plugin_name plugin_supports_json supported
local records_tmp plugin_name plugin_supports_json supported
plugins_tmp="$(mktemp)" || return 1
records_tmp="$(mktemp)" || {
rm -f "$plugins_tmp"
return 1
}
printf '%s' "$outbounds_json" | jq -r '
.[]?
| select(.type == "shadowsocks")
| (.plugin // empty)
| tostring
| split(";")[0]
| select(. != "")
' 2>/dev/null | sort -u > "$plugins_tmp"
records_tmp="$(mktemp)" || return 1
: > "$records_tmp"
sing_box_cf_ucode_input plugin-names "$outbounds_json" 2>/dev/null |
while IFS= read -r plugin_name || [ -n "$plugin_name" ]; do
[ -n "$plugin_name" ] || continue
command -v "$plugin_name" >/dev/null 2>&1 && supported=true || supported=false
printf '%s\t%s\n' "$plugin_name" "$supported" >> "$records_tmp"
done < "$plugins_tmp"
done
plugin_supports_json="$(jq -Rn '
reduce inputs as $line (
{};
($line | split("\t")) as $parts
| if ($parts | length) >= 2 and $parts[0] != "" then
.[$parts[0]] = ($parts[1] == "true")
else
.
end
)
' < "$records_tmp" 2>/dev/null)"
plugin_supports_json="$(sing_box_cf_ucode plugin-supports-from-records "$records_tmp" 2>/dev/null)"
rm -f "$plugins_tmp" "$records_tmp"
rm -f "$records_tmp"
[ -n "$plugin_supports_json" ] || plugin_supports_json="{}"
printf '%s\n' "$plugin_supports_json"
}
@@ -444,8 +500,7 @@ sing_box_cf_subscription_plugin_supports() {
sing_box_cf_prepare_subscription_batch() {
local config="$1"
local outbounds_json="$2"
local existing_tags_json existing_tags_tmp outbounds_tmp prepared_json jq_status supports_xhttp plugin_supports_json \
parser_path config_tmp plugin_supports_tmp prepared_tmp
local outbounds_tmp prepared_json supports_xhttp plugin_supports_json config_tmp plugin_supports_tmp prepared_tmp
supports_xhttp=false
if is_sing_box_extended; then
@@ -454,189 +509,42 @@ sing_box_cf_prepare_subscription_batch() {
plugin_supports_json="$(sing_box_cf_subscription_plugin_supports "$outbounds_json")"
[ -n "$plugin_supports_json" ] || plugin_supports_json="{}"
existing_tags_json="$(printf '%s' "$config" | jq -c '[.outbounds[]?.tag // empty]' 2>/dev/null)"
[ -n "$existing_tags_json" ] || existing_tags_json="[]"
existing_tags_tmp="$(mktemp)" || return 1
outbounds_tmp="$(mktemp)" || {
rm -f "$existing_tags_tmp"
return 1
}
printf '%s' "$existing_tags_json" > "$existing_tags_tmp" || {
rm -f "$existing_tags_tmp" "$outbounds_tmp"
return 1
}
outbounds_tmp="$(mktemp)" || return 1
printf '%s' "$outbounds_json" > "$outbounds_tmp" || {
rm -f "$existing_tags_tmp" "$outbounds_tmp"
rm -f "$outbounds_tmp"
return 1
}
parser_path="${PODKOP_LIB:-/usr/lib/podkop-plus}/subscription_parser.lua"
if command -v lua >/dev/null 2>&1 && [ -r "$parser_path" ]; then
config_tmp="$(mktemp)" || {
rm -f "$existing_tags_tmp" "$outbounds_tmp"
return 1
}
plugin_supports_tmp="$(mktemp)" || {
rm -f "$existing_tags_tmp" "$outbounds_tmp" "$config_tmp"
return 1
}
prepared_tmp="$(mktemp)" || {
rm -f "$existing_tags_tmp" "$outbounds_tmp" "$config_tmp" "$plugin_supports_tmp"
return 1
}
command -v ucode >/dev/null 2>&1 || {
rm -f "$outbounds_tmp"
return 1
}
if printf '%s' "$config" > "$config_tmp" &&
printf '%s' "$plugin_supports_json" > "$plugin_supports_tmp" &&
lua "$parser_path" prepare "$config_tmp" "$outbounds_tmp" "$prepared_tmp" "$supports_xhttp" "$plugin_supports_tmp" &&
prepared_json="$(cat "$prepared_tmp" 2>/dev/null)" &&
[ -n "$prepared_json" ]; then
rm -f "$existing_tags_tmp" "$outbounds_tmp" "$config_tmp" "$plugin_supports_tmp" "$prepared_tmp"
printf '%s\n' "$prepared_json"
return 0
fi
config_tmp="$(mktemp)" || {
rm -f "$outbounds_tmp"
return 1
}
plugin_supports_tmp="$(mktemp)" || {
rm -f "$outbounds_tmp" "$config_tmp"
return 1
}
prepared_tmp="$(mktemp)" || {
rm -f "$outbounds_tmp" "$config_tmp" "$plugin_supports_tmp"
return 1
}
rm -f "$config_tmp" "$plugin_supports_tmp" "$prepared_tmp"
log "Lua subscription batch preparation failed; falling back to jq preparation" "warn"
if printf '%s' "$config" > "$config_tmp" &&
printf '%s' "$plugin_supports_json" > "$plugin_supports_tmp" &&
sing_box_cf_ucode prepare-subscription "$config_tmp" "$outbounds_tmp" "$prepared_tmp" "$supports_xhttp" "$plugin_supports_tmp" &&
prepared_json="$(cat "$prepared_tmp" 2>/dev/null)" &&
[ -n "$prepared_json" ]; then
rm -f "$outbounds_tmp" "$config_tmp" "$plugin_supports_tmp" "$prepared_tmp"
printf '%s\n' "$prepared_json"
return 0
fi
prepared_json="$(jq -c --slurpfile existing_tags "$existing_tags_tmp" \
--argjson supports_xhttp "$supports_xhttp" \
--argjson plugin_supports "$plugin_supports_json" '
($existing_tags[0] // []) as $existing_tags
|
def safe_string($value; $fallback):
(($value // $fallback) | tostring) as $result
| if $result == "" or $result == "null" then $fallback else $result end;
def non_empty_string($value):
($value | type) == "string" and $value != "";
def valid_server($outbound):
non_empty_string($outbound.server // null);
def valid_server_port($outbound):
($outbound.server_port | type) == "number" and
(($outbound.server_port | floor) == $outbound.server_port) and
$outbound.server_port >= 1 and
$outbound.server_port <= 65535;
def type_requires_server($type):
["vless", "vmess", "trojan", "shadowsocks", "socks", "hysteria2"] | index($type) != null;
def supported_flow($flow):
($flow == null) or ($flow == "") or ($flow == "xtls-rprx-vision");
def supported_transport_type($transport):
["http", "ws", "quic", "grpc", "httpupgrade", "xhttp", "kcp"] | index($transport) != null;
def supported_shadowsocks_method($method):
[
"none",
"aes-128-gcm", "aes-192-gcm", "aes-256-gcm",
"chacha20-ietf-poly1305", "xchacha20-ietf-poly1305",
"2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305",
"aes-128-cfb", "aes-192-cfb", "aes-256-cfb",
"aes-128-ctr", "aes-192-ctr", "aes-256-ctr",
"chacha20", "chacha20-ietf", "xchacha20",
"salsa20", "rc4-md5"
] | index($method) != null;
def reality_enabled($outbound):
($outbound.tls.reality? | type) == "object" and (($outbound.tls.reality.enabled // true) == true);
def plugin_name($plugin):
($plugin | tostring | split(";")[0]);
def prefilter_skip_reason($outbound):
($outbound.type // "" | tostring) as $type
| if $type == "" then
"missing outbound type"
elif ($outbound.type != ($outbound.type | tostring)) then
"outbound type must be a string"
elif type_requires_server($type) and (valid_server($outbound) | not) then
"missing or empty server"
elif type_requires_server($type) and (valid_server_port($outbound) | not) then
"missing or invalid server_port"
elif ($type == "vless" or $type == "vmess") and (non_empty_string($outbound.uuid // null) | not) then
"missing uuid"
elif ($type == "trojan" or $type == "hysteria2") and (non_empty_string($outbound.password // null) | not) then
"missing password"
elif $type == "shadowsocks" and (non_empty_string($outbound.method // null) | not) then
"missing shadowsocks method"
elif $type == "shadowsocks" and (non_empty_string($outbound.password // null) | not) then
"missing shadowsocks password"
elif $type == "shadowsocks" and (supported_shadowsocks_method($outbound.method) | not) then
"unsupported shadowsocks method: \($outbound.method)"
elif $type == "shadowsocks" and non_empty_string($outbound.plugin // null) and (($plugin_supports[plugin_name($outbound.plugin)] // false) | not) then
"shadowsocks plugin is not installed: \(plugin_name($outbound.plugin))"
elif reality_enabled($outbound) and (non_empty_string($outbound.tls.reality.public_key // null) | not) then
"reality public_key is missing"
elif ($type == "vless") and (supported_flow($outbound.flow // "") | not) then
"unsupported vless flow: \($outbound.flow)"
elif (($outbound.transport? | type) == "object") and (($outbound.transport.type // "" | tostring) == "") then
"missing transport type"
elif (($outbound.transport? | type) == "object") and (supported_transport_type(($outbound.transport.type // "") | tostring) | not) then
"unknown transport type: \($outbound.transport.type)"
elif (($outbound.transport? | type) == "object") and (($outbound.transport.type // "" | tostring) == "xhttp") and ($supports_xhttp | not) then
"transport xhttp requires sing-box-extended"
elif ($type == "shadowsocks" and (($outbound.tls.enabled // false) == true)) then
"shadowsocks with TLS is not supported"
else
""
end;
def unique_tag($base; $taken):
if (($taken[$base] // false) | not) then
$base
else
first(range(1; 100000) as $n
| "\($base)-\($n)"
| select((($taken[.] // false) | not)))
end;
. as $subscription_outbounds |
reduce range(0; ($subscription_outbounds | length)) as $source_index (
{
outbounds: [],
tags: [],
names: [],
servers: [],
links: [],
source_indices: [],
skipped: 0,
skipped_reason_counts: {},
taken: (reduce $existing_tags[] as $tag ({}; .[$tag] = true))
};
$subscription_outbounds[$source_index] as $outbound
|
((.outbounds | length) + 1) as $index
| safe_string($outbound.remark // $outbound.tag; "server-\($index)") as $display_name
| prefilter_skip_reason($outbound) as $skip_reason
| if $skip_reason != "" then
.skipped += 1
| .skipped_reason_counts[$skip_reason] = ((.skipped_reason_counts[$skip_reason] // 0) + 1)
else
safe_string($outbound.tag // $outbound.remark; "server-\($index)") as $base_tag
| (.taken as $taken | unique_tag($base_tag; $taken)) as $tag
| .outbounds += [($outbound | del(.tag, .remark, .share_link) + {tag: $tag})]
| .tags += [$tag]
| .names += [$display_name]
| .servers += [($outbound.server // "")]
| .links += [($outbound.share_link // "")]
| .source_indices += [($source_index + 1)]
| .taken[$tag] = true
end
)
| del(.taken)
' "$outbounds_tmp" 2>/dev/null)"
jq_status=$?
rm -f "$existing_tags_tmp" "$outbounds_tmp"
[ "$jq_status" -eq 0 ] || return 1
[ -n "$prepared_json" ] || return 1
printf '%s\n' "$prepared_json"
rm -f "$outbounds_tmp" "$config_tmp" "$plugin_supports_tmp" "$prepared_tmp"
return 1
}
sing_box_cf_validation_error_summary() {
@@ -654,52 +562,47 @@ sing_box_cf_validation_error_summary() {
sing_box_cf_try_subscription_outbounds_batch() {
local config="$1"
local new_outbounds="$2"
local new_outbounds_tmp config_tmp validation_tmp validation_config updated_config jq_status check_output
local new_outbounds_tmp updated_tmp validation_tmp check_tmp validation_config check_output
SING_BOX_CF_VALIDATED_CONFIG=""
SING_BOX_CF_VALIDATION_ERROR=""
new_outbounds_tmp="$(mktemp)" || return 1
config_tmp="$(mktemp)" || {
updated_tmp="$(mktemp)" || {
rm -f "$new_outbounds_tmp"
return 1
}
printf '%s' "$new_outbounds" > "$new_outbounds_tmp" || {
rm -f "$new_outbounds_tmp" "$config_tmp"
validation_tmp="$(mktemp)" || {
rm -f "$new_outbounds_tmp" "$updated_tmp"
return 1
}
printf '%s' "$config" > "$config_tmp" || {
rm -f "$new_outbounds_tmp" "$config_tmp"
check_tmp="$(mktemp)" || {
rm -f "$new_outbounds_tmp" "$updated_tmp" "$validation_tmp"
return 1
}
updated_config="$(jq -c --slurpfile new_outbounds "$new_outbounds_tmp" \
'.outbounds += ($new_outbounds[0] // [])' "$config_tmp" 2>/dev/null)"
jq_status=$?
rm -f "$new_outbounds_tmp" "$config_tmp"
[ "$jq_status" -eq 0 ] || return 1
[ -n "$updated_config" ] || return 1
updated_config="$(printf '%s' "$updated_config" | jq -c 'del(.outbounds[]?.share_link, .outbounds[]?.remark)' 2>/dev/null)" || return 1
[ -n "$updated_config" ] || return 1
validation_config="$(printf '%s' "$updated_config" | jq -c '
(first(.outbounds[]? | select(.type == "direct") | .tag) // "direct-out") as $direct
| .route = ((.route // {}) + {rules: [], rule_set: [], final: $direct})
' 2>/dev/null)" || return 1
[ -n "$validation_config" ] || return 1
validation_tmp="$(mktemp)" || return 1
sing_box_cm_save_config_to_file "$validation_config" "$validation_tmp"
if ! check_output="$(sing-box -c "$validation_tmp" check 2>&1)"; then
SING_BOX_CF_VALIDATION_ERROR="$(sing_box_cf_validation_error_summary "$check_output")"
rm -f "$validation_tmp"
if ! printf '%s' "$new_outbounds" > "$new_outbounds_tmp" ||
! sing_box_cf_ucode_input prepare-validation "$config" "$new_outbounds_tmp" "$updated_tmp" "$validation_tmp"; then
rm -f "$new_outbounds_tmp" "$updated_tmp" "$validation_tmp" "$check_tmp"
return 1
fi
rm -f "$validation_tmp"
SING_BOX_CF_VALIDATED_CONFIG="$updated_config"
validation_config="$(cat "$validation_tmp" 2>/dev/null)" || validation_config=""
[ -n "$validation_config" ] || {
rm -f "$new_outbounds_tmp" "$updated_tmp" "$validation_tmp" "$check_tmp"
return 1
}
sing_box_cm_save_config_to_file "$validation_config" "$check_tmp"
if ! check_output="$(sing-box -c "$check_tmp" check 2>&1)"; then
SING_BOX_CF_VALIDATION_ERROR="$(sing_box_cf_validation_error_summary "$check_output")"
rm -f "$new_outbounds_tmp" "$updated_tmp" "$validation_tmp" "$check_tmp"
return 1
fi
SING_BOX_CF_VALIDATED_CONFIG="$(cat "$updated_tmp" 2>/dev/null)"
rm -f "$new_outbounds_tmp" "$updated_tmp" "$validation_tmp" "$check_tmp"
[ -n "$SING_BOX_CF_VALIDATED_CONFIG" ] || return 1
return 0
}
@@ -707,55 +610,25 @@ sing_box_cf_prepared_link_refs_json() {
local prepared_json="$1"
local source_section="${2:-}"
printf '%s' "$prepared_json" | jq -c --arg source_section "$source_section" '
(.tags // []) as $tags
| (.source_indices // []) as $source_indices
| $source_section as $source_section
| reduce range(0; ($tags | length)) as $index (
{};
if $source_section != "" then
.[$tags[$index]] = {
sourceSection: $source_section,
sourceIndex: (($source_indices[$index] // ($index + 1)) | tonumber)
}
else
.
end
)
' 2>/dev/null
sing_box_cf_ucode_input prepared-link-refs "$prepared_json" "$source_section" 2>/dev/null
}
sing_box_cf_prepared_names_json() {
local prepared_json="$1"
printf '%s' "$prepared_json" | jq -c '
(.tags // []) as $tags
| (.names // []) as $names
| reduce range(0; ($tags | length)) as $index (
{};
.[$tags[$index]] = (($names[$index] // $tags[$index]) | tostring)
)
' 2>/dev/null
sing_box_cf_ucode_input prepared-names "$prepared_json" 2>/dev/null
}
sing_box_cf_prepared_servers_json() {
local prepared_json="$1"
printf '%s' "$prepared_json" | jq -c '
(.tags // []) as $tags
| (.servers // []) as $servers
| reduce range(0; ($tags | length)) as $index (
{};
(($servers[$index] // "") | tostring) as $server
| if $server != "" then .[$tags[$index]] = $server else . end
)
' 2>/dev/null
sing_box_cf_ucode_input prepared-servers "$prepared_json" 2>/dev/null
}
sing_box_cf_prepared_names_lines() {
local prepared_json="$1"
printf '%s' "$prepared_json" | jq -r '.names[]?' 2>/dev/null
sing_box_cf_ucode_input prepared-names-lines "$prepared_json" 2>/dev/null
}
sing_box_cf_subscription_prepared_slice() {
@@ -763,25 +636,14 @@ sing_box_cf_subscription_prepared_slice() {
local start="$2"
local end="$3"
printf '%s' "$prepared_json" | jq -c --argjson start "$start" --argjson end "$end" '
. + {
outbounds: ((.outbounds // [])[$start:$end]),
tags: ((.tags // [])[$start:$end]),
names: ((.names // [])[$start:$end]),
servers: ((.servers // [])[$start:$end]),
links: ((.links // [])[$start:$end]),
source_indices: ((.source_indices // [])[$start:$end]),
skipped: 0,
skipped_reason_counts: {}
}
' 2>/dev/null
sing_box_cf_ucode_input prepared-slice "$prepared_json" "$start" "$end" 2>/dev/null
}
sing_box_cf_append_subscription_prepared_metadata() {
local prepared_json="$1"
local tags_json link_refs_json names_json servers_json names
tags_json="$(printf '%s' "$prepared_json" | jq -c '.tags // []' 2>/dev/null)"
tags_json="$(sing_box_cf_ucode_input prepared-field "$prepared_json" tags 2>/dev/null)"
[ -n "$tags_json" ] || tags_json="[]"
link_refs_json="$(sing_box_cf_prepared_link_refs_json "$prepared_json" "$SING_BOX_CF_SOURCE_SECTION")"
@@ -791,20 +653,16 @@ sing_box_cf_append_subscription_prepared_metadata() {
servers_json="$(sing_box_cf_prepared_servers_json "$prepared_json")"
[ -n "$servers_json" ] || servers_json="{}"
SUBSCRIPTION_OUTBOUND_TAGS_JSON="$(printf '%s' "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" |
jq -c --argjson tags "$tags_json" '. + $tags' 2>/dev/null)"
SUBSCRIPTION_OUTBOUND_TAGS_JSON="$(sing_box_cf_json_arrays_concat "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" "$tags_json" 2>/dev/null)"
[ -n "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" ] || SUBSCRIPTION_OUTBOUND_TAGS_JSON="[]"
SUBSCRIPTION_OUTBOUND_LINK_REFS_JSON="$(printf '%s' "$SUBSCRIPTION_OUTBOUND_LINK_REFS_JSON" |
jq -c --argjson link_refs "$link_refs_json" '. + $link_refs' 2>/dev/null)"
SUBSCRIPTION_OUTBOUND_LINK_REFS_JSON="$(sing_box_cf_json_objects_merge "$SUBSCRIPTION_OUTBOUND_LINK_REFS_JSON" "$link_refs_json" 2>/dev/null)"
[ -n "$SUBSCRIPTION_OUTBOUND_LINK_REFS_JSON" ] || SUBSCRIPTION_OUTBOUND_LINK_REFS_JSON="{}"
SUBSCRIPTION_OUTBOUND_NAMES_JSON="$(printf '%s' "$SUBSCRIPTION_OUTBOUND_NAMES_JSON" |
jq -c --argjson names "$names_json" '. + $names' 2>/dev/null)"
SUBSCRIPTION_OUTBOUND_NAMES_JSON="$(sing_box_cf_json_objects_merge "$SUBSCRIPTION_OUTBOUND_NAMES_JSON" "$names_json" 2>/dev/null)"
[ -n "$SUBSCRIPTION_OUTBOUND_NAMES_JSON" ] || SUBSCRIPTION_OUTBOUND_NAMES_JSON="{}"
SUBSCRIPTION_OUTBOUND_SERVERS_JSON="$(printf '%s' "$SUBSCRIPTION_OUTBOUND_SERVERS_JSON" |
jq -c --argjson servers "$servers_json" '. + $servers' 2>/dev/null)"
SUBSCRIPTION_OUTBOUND_SERVERS_JSON="$(sing_box_cf_json_objects_merge "$SUBSCRIPTION_OUTBOUND_SERVERS_JSON" "$servers_json" 2>/dev/null)"
[ -n "$SUBSCRIPTION_OUTBOUND_SERVERS_JSON" ] || SUBSCRIPTION_OUTBOUND_SERVERS_JSON="{}"
names="$(sing_box_cf_prepared_names_lines "$prepared_json")"
@@ -823,9 +681,9 @@ sing_box_cf_apply_subscription_batch() {
local prepared_json="$2"
local new_outbounds new_outbounds_count
new_outbounds="$(printf '%s' "$prepared_json" | jq -c '.outbounds // []' 2>/dev/null)"
new_outbounds="$(sing_box_cf_ucode_input prepared-field "$prepared_json" outbounds 2>/dev/null)"
[ -n "$new_outbounds" ] || return 1
new_outbounds_count="$(printf '%s' "$new_outbounds" | jq -r 'length' 2>/dev/null)"
new_outbounds_count="$(sing_box_cf_ucode_input prepared-length "$prepared_json" outbounds 2>/dev/null)"
[ -n "$new_outbounds_count" ] || return 1
[ "$new_outbounds_count" -gt 0 ] || return 1
@@ -833,9 +691,9 @@ sing_box_cf_apply_subscription_batch() {
return 1
fi
SUBSCRIPTION_OUTBOUND_TAGS_JSON="$(printf '%s' "$prepared_json" | jq -c '.tags // []' 2>/dev/null)"
SUBSCRIPTION_OUTBOUND_TAGS_JSON="$(sing_box_cf_ucode_input prepared-field "$prepared_json" tags 2>/dev/null)"
[ -n "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" ] || SUBSCRIPTION_OUTBOUND_TAGS_JSON="[]"
SUBSCRIPTION_OUTBOUND_TAGS="$(printf '%s' "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" | jq -r 'join(",")' 2>/dev/null)"
SUBSCRIPTION_OUTBOUND_TAGS="$(sing_box_cf_ucode_input prepared-tags-csv "$prepared_json" 2>/dev/null)"
SUBSCRIPTION_OUTBOUND_NAMES="$(sing_box_cf_prepared_names_lines "$prepared_json")"
SUBSCRIPTION_OUTBOUND_LINK_REFS_JSON="$(sing_box_cf_prepared_link_refs_json "$prepared_json" "$SING_BOX_CF_SOURCE_SECTION")"
[ -n "$SUBSCRIPTION_OUTBOUND_LINK_REFS_JSON" ] || SUBSCRIPTION_OUTBOUND_LINK_REFS_JSON="{}"
@@ -861,7 +719,7 @@ sing_box_cf_apply_subscription_outbounds_range() {
return 0
}
outbounds_json="$(printf '%s' "$chunk" | jq -c '.outbounds // []' 2>/dev/null)"
outbounds_json="$(sing_box_cf_ucode_input prepared-field "$chunk" outbounds 2>/dev/null)"
[ -n "$outbounds_json" ] || {
SING_BOX_CF_FALLBACK_SKIPPED_COUNT=$((SING_BOX_CF_FALLBACK_SKIPPED_COUNT + count))
return 0
@@ -875,10 +733,7 @@ sing_box_cf_apply_subscription_outbounds_range() {
fi
if [ "$count" -eq 1 ]; then
display_name="$(printf '%s' "$chunk" |
jq -r '(.names[0] // .outbounds[0].tag // "unknown") | tostring' 2>/dev/null |
tr '\r\n\t' ' ' |
sed 's/[[:space:]][[:space:]]*/ /g;s/^[[:space:]]*//;s/[[:space:]]*$//')"
display_name="$(sing_box_cf_ucode_input prepared-display-name "$chunk" 2>/dev/null)"
[ -n "$display_name" ] || display_name="unknown"
[ -n "$SING_BOX_CF_VALIDATION_ERROR" ] || SING_BOX_CF_VALIDATION_ERROR="sing-box check failed"
log "Skipped unsupported subscription outbound '$display_name': $SING_BOX_CF_VALIDATION_ERROR" "warn"
@@ -908,7 +763,7 @@ sing_box_cf_apply_subscription_outbounds_chunked() {
local prepared_json="$2"
local outbounds_count half rest
outbounds_count="$(printf '%s' "$prepared_json" | jq -r '(.outbounds // []) | length' 2>/dev/null)"
outbounds_count="$(sing_box_cf_ucode_input prepared-length "$prepared_json" outbounds 2>/dev/null)"
[ -n "$outbounds_count" ] || return 1
[ "$outbounds_count" -gt 0 ] || return 1
@@ -940,7 +795,7 @@ sing_box_cf_apply_subscription_outbounds_chunked() {
log "Skipped $SING_BOX_CF_FALLBACK_SKIPPED_COUNT unsupported subscription outbounds during chunked fallback validation" "warn"
fi
SUBSCRIPTION_OUTBOUND_TAGS="$(printf '%s' "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" | jq -r 'join(",")' 2>/dev/null)"
SUBSCRIPTION_OUTBOUND_TAGS="$(sing_box_cf_tags_json_csv "$SUBSCRIPTION_OUTBOUND_TAGS_JSON")"
SING_BOX_CF_LAST_CONFIG="$SING_BOX_CF_FALLBACK_WORKING_CONFIG"
return 0
@@ -969,7 +824,7 @@ sing_box_cf_add_subscription_outbounds() {
fi
outbounds_json="$(sing_box_cf_subscription_candidate_outbounds "$subscription_json_path")"
outbounds_count="$(printf '%s' "$outbounds_json" | jq -r 'length' 2>/dev/null)"
outbounds_count="$(sing_box_cf_outbounds_json_count "$outbounds_json")"
if [ -z "$outbounds_count" ] || [ "$outbounds_count" -eq 0 ]; then
log "No proxy outbounds found in subscription JSON" "error"
@@ -981,13 +836,13 @@ sing_box_cf_add_subscription_outbounds() {
prepared_json="$(sing_box_cf_prepare_subscription_batch "$config" "$outbounds_json")"
if [ -n "$prepared_json" ]; then
skipped_count="$(printf '%s' "$prepared_json" | jq -r '.skipped // 0' 2>/dev/null)"
skipped_count="$(sing_box_cf_ucode_input skip-count "$prepared_json" 2>/dev/null)"
if [ "${skipped_count:-0}" -gt 0 ]; then
sing_box_cf_log_subscription_skips "$prepared_json" "before validation"
fi
if sing_box_cf_apply_subscription_batch "$config" "$prepared_json"; then
log "Added $(printf '%s' "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" | jq -r 'length' 2>/dev/null) subscription outbounds for rule '$section'" "info"
log "Added $(sing_box_cf_tags_json_count "$SUBSCRIPTION_OUTBOUND_TAGS_JSON") subscription outbounds for rule '$section'" "info"
echo "$SING_BOX_CF_LAST_CONFIG"
return 0
fi
@@ -995,7 +850,7 @@ sing_box_cf_add_subscription_outbounds() {
log "Batch subscription validation failed for rule '$section', trying chunked fallback validation" "warn"
if [ -n "$prepared_json" ] && sing_box_cf_apply_subscription_outbounds_chunked "$config" "$prepared_json"; then
log "Added $(printf '%s' "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" | jq -r 'length' 2>/dev/null) subscription outbounds for rule '$section'" "info"
log "Added $(sing_box_cf_tags_json_count "$SUBSCRIPTION_OUTBOUND_TAGS_JSON") subscription outbounds for rule '$section'" "info"
echo "$SING_BOX_CF_LAST_CONFIG"
return 0
fi
@@ -0,0 +1,516 @@
#!/usr/bin/env ucode
let fs = require("fs");
function as_string(value) {
return value == null ? "" : "" + value;
}
function read_stdin() {
let input = fs.open("/dev/stdin", "r");
if (!input)
return "";
let data = input.read("all");
input.close();
return data == null ? "" : data;
}
function json_decode_text(text) {
try {
return json(as_string(text));
}
catch (e) {
return null;
}
}
function read_stdin_json() {
return json_decode_text(read_stdin());
}
function read_json_file(path) {
let data = fs.readfile(path);
return data == null ? null : json_decode_text(data);
}
function write_json(value) {
print(sprintf("%J", value), "\n");
}
function write_file_json(path, value) {
return fs.writefile(path, sprintf("%J", value) + "\n");
}
function array_or_empty(value) {
return type(value) == "array" ? value : [];
}
function object_or_empty(value) {
return type(value) == "object" ? value : {};
}
function sort_strings(values) {
sort(values, function(first, second) {
return first == second ? 0 : (first < second ? -1 : 1);
});
return values;
}
function clone(value) {
return json_decode_text(sprintf("%J", value));
}
function non_empty_string(value) {
return type(value) == "string" && value != "";
}
function valid_server_port(value) {
return type(value) == "int" && value >= 1 && value <= 65535;
}
function type_requires_server(proxy_type) {
return proxy_type == "vless" || proxy_type == "vmess" || proxy_type == "trojan" ||
proxy_type == "shadowsocks" || proxy_type == "socks" || proxy_type == "hysteria2";
}
function supported_flow(flow) {
return flow == null || flow == "" || flow == "xtls-rprx-vision";
}
function supported_transport_type(transport) {
return transport == "http" || transport == "ws" || transport == "quic" || transport == "grpc" ||
transport == "httpupgrade" || transport == "xhttp" || transport == "kcp";
}
let supported_shadowsocks_methods = {
"none": true,
"aes-128-gcm": true,
"aes-192-gcm": true,
"aes-256-gcm": true,
"chacha20-ietf-poly1305": true,
"xchacha20-ietf-poly1305": true,
"2022-blake3-aes-128-gcm": true,
"2022-blake3-aes-256-gcm": true,
"2022-blake3-chacha20-poly1305": true,
"aes-128-cfb": true,
"aes-192-cfb": true,
"aes-256-cfb": true,
"aes-128-ctr": true,
"aes-192-ctr": true,
"aes-256-ctr": true,
"chacha20": true,
"chacha20-ietf": true,
"xchacha20": true,
"salsa20": true,
"rc4-md5": true
};
function plugin_name(plugin) {
let m = match(as_string(plugin), /^([^;]*)/);
return m ? m[1] : "";
}
function reality_enabled(outbound) {
return type(outbound.tls) == "object" && type(outbound.tls.reality) == "object" &&
(outbound.tls.reality.enabled == null || outbound.tls.reality.enabled === true);
}
function prefilter_skip_reason(outbound, supports_xhttp, plugin_supports) {
if (type(outbound) != "object")
return "not an object";
let proxy_type = as_string(outbound.type || "");
if (proxy_type == "")
return "missing type";
if (proxy_type == "direct" || proxy_type == "selector" || proxy_type == "urltest" ||
proxy_type == "dns" || proxy_type == "block")
return "non-proxy outbound type: " + proxy_type;
if (proxy_type != "vless" && proxy_type != "vmess" && proxy_type != "trojan" &&
proxy_type != "shadowsocks" && proxy_type != "socks" && proxy_type != "hysteria2")
return "unsupported type: " + proxy_type;
if (type_requires_server(proxy_type) && !non_empty_string(outbound.server))
return "missing or empty server";
if (type_requires_server(proxy_type) && !valid_server_port(outbound.server_port))
return "missing or invalid server_port";
if ((proxy_type == "vless" || proxy_type == "vmess") && !non_empty_string(outbound.uuid))
return "missing uuid";
if ((proxy_type == "trojan" || proxy_type == "hysteria2") && !non_empty_string(outbound.password))
return "missing password";
if (proxy_type == "shadowsocks" && !non_empty_string(outbound.method))
return "missing shadowsocks method";
if (proxy_type == "shadowsocks" && !non_empty_string(outbound.password))
return "missing shadowsocks password";
if (proxy_type == "shadowsocks" && !supported_shadowsocks_methods[outbound.method])
return "unsupported shadowsocks method: " + as_string(outbound.method);
if (proxy_type == "shadowsocks" && non_empty_string(outbound.plugin) && !plugin_supports[plugin_name(outbound.plugin)])
return "shadowsocks plugin is not installed: " + plugin_name(outbound.plugin);
if (reality_enabled(outbound) && !non_empty_string(outbound.tls.reality.public_key))
return "reality public_key is missing";
if (proxy_type == "vless" && !supported_flow(outbound.flow || ""))
return "unsupported vless flow: " + as_string(outbound.flow);
if (type(outbound.transport) == "object" && as_string(outbound.transport.type || "") == "")
return "missing transport type";
if (type(outbound.transport) == "object" && !supported_transport_type(as_string(outbound.transport.type || "")))
return "unknown transport type: " + as_string(outbound.transport.type);
if (type(outbound.transport) == "object" && as_string(outbound.transport.type || "") == "xhttp" && !supports_xhttp)
return "transport xhttp requires sing-box-extended";
if (proxy_type == "shadowsocks" && type(outbound.tls) == "object" && outbound.tls.enabled === true)
return "shadowsocks with TLS is not supported";
return "";
}
function safe_string(value, fallback) {
let result = value == null ? fallback : as_string(value);
return result == "" || result == "null" ? fallback : result;
}
function unique_tag(base, taken) {
if (!taken[base])
return base;
for (let suffix = 1; suffix < 100000; suffix++) {
let candidate = base + "-" + suffix;
if (!taken[candidate])
return candidate;
}
return base + "-overflow";
}
function copy_outbound(outbound) {
let copy = {};
for (let key, value in outbound) {
if (key != "tag" && key != "remark" && key != "share_link")
copy[key] = value;
}
return copy;
}
function candidate_outbounds(path) {
let subscription = object_or_empty(read_json_file(path));
let result = [];
let skipped_types = {
selector: true,
urltest: true,
direct: true,
dns: true,
block: true
};
for (let outbound in array_or_empty(subscription.outbounds)) {
if (type(outbound) == "object" && !skipped_types[outbound.type])
push(result, outbound);
}
write_json(result);
}
function prepare_subscription(config_path, outbounds_path, output_path, supports_xhttp_arg, plugin_supports_path) {
let config = read_json_file(config_path);
let outbounds = read_json_file(outbounds_path);
let plugin_supports = object_or_empty(read_json_file(plugin_supports_path));
if (type(config) != "object" || type(outbounds) != "array")
exit(1);
let supports_xhttp = supports_xhttp_arg == "true" || supports_xhttp_arg == "1";
let taken = {};
for (let outbound in array_or_empty(config.outbounds)) {
if (type(outbound) == "object" && non_empty_string(outbound.tag))
taken[outbound.tag] = true;
}
let tags = [];
let names = [];
let servers = [];
let links = [];
let source_indices = [];
let skipped = 0;
let skipped_reason_counts = {};
let prepared = [];
for (let i = 0; i < length(outbounds); i++) {
let outbound = outbounds[i];
let index = length(prepared) + 1;
let display_name = safe_string(outbound && (outbound.remark || outbound.tag), "server-" + index);
let skip_reason = prefilter_skip_reason(outbound, supports_xhttp, plugin_supports);
if (skip_reason != "") {
skipped++;
skipped_reason_counts[skip_reason] = (skipped_reason_counts[skip_reason] || 0) + 1;
continue;
}
let base_tag = safe_string(outbound.tag || outbound.remark, "server-" + index);
let tag = unique_tag(base_tag, taken);
let outbound_copy = copy_outbound(outbound);
outbound_copy.tag = tag;
push(prepared, outbound_copy);
push(tags, tag);
push(names, display_name);
push(servers, outbound.server || "");
push(links, outbound.share_link || "");
push(source_indices, i + 1);
taken[tag] = true;
}
if (!write_file_json(output_path, {
outbounds: prepared,
links,
source_indices,
names,
servers,
skipped,
skipped_reason_counts: length(keys(skipped_reason_counts)) > 0 ? skipped_reason_counts : [],
tags
}))
exit(1);
}
function skip_count() {
let prepared = object_or_empty(read_stdin_json());
print(as_string(prepared.skipped || 0), "\n");
}
function skip_summary() {
let prepared = object_or_empty(read_stdin_json());
let counts = object_or_empty(prepared.skipped_reason_counts);
let entries = [];
for (let reason, count in counts) {
push(entries, { reason, count: int(count || 0) });
}
sort(entries, function(first, second) {
if (first.count != second.count)
return second.count - first.count;
return first.reason == second.reason ? 0 : (first.reason < second.reason ? -1 : 1);
});
let parts = [];
for (let entry in entries)
push(parts, entry.count + "x " + entry.reason);
print(join("; ", parts), "\n");
}
function plugin_names() {
let outbounds = array_or_empty(read_stdin_json());
let seen = {};
for (let outbound in outbounds) {
if (type(outbound) != "object" || outbound.type != "shadowsocks")
continue;
let plugin = as_string(outbound.plugin);
let semicolon = index(plugin, ";");
if (semicolon >= 0)
plugin = substr(plugin, 0, semicolon);
if (plugin != "")
seen[plugin] = true;
}
for (let name in sort_strings(keys(seen)))
print(name, "\n");
}
function plugin_supports_from_records(path) {
let result = {};
let data = fs.readfile(path);
if (data != null) {
for (let line in split(data, "\n")) {
if (line == "")
continue;
let parts = split(line, "\t");
if (length(parts) >= 2 && parts[0] != "")
result[parts[0]] = parts[1] == "true";
}
}
write_json(result);
}
function first_direct_tag(config) {
for (let outbound in array_or_empty(config.outbounds)) {
if (type(outbound) == "object" && outbound.type == "direct" && as_string(outbound.tag) != "")
return outbound.tag;
}
return "direct-out";
}
function strip_runtime_fields(config) {
for (let outbound in array_or_empty(config.outbounds)) {
if (type(outbound) == "object") {
delete outbound.share_link;
delete outbound.remark;
}
}
}
function prepare_validation(new_outbounds_path, updated_path, validation_path) {
let config = object_or_empty(read_stdin_json());
let new_outbounds = array_or_empty(read_json_file(new_outbounds_path));
if (type(config.outbounds) != "array")
config.outbounds = [];
for (let outbound in new_outbounds)
push(config.outbounds, outbound);
strip_runtime_fields(config);
let validation = clone(config);
let route = type(validation.route) == "object" ? validation.route : {};
route.rules = [];
route.rule_set = [];
route.final = first_direct_tag(config);
validation.route = route;
if (!write_file_json(updated_path, config))
exit(1);
if (!write_file_json(validation_path, validation))
exit(1);
}
function prepared_link_refs(prepared, source_section) {
let tags = array_or_empty(prepared.tags);
let source_indices = array_or_empty(prepared.source_indices);
let result = {};
if (source_section == "")
return result;
for (let i = 0; i < length(tags); i++) {
result[tags[i]] = {
sourceSection: source_section,
sourceIndex: int(source_indices[i] || (i + 1))
};
}
return result;
}
function prepared_names(prepared) {
let tags = array_or_empty(prepared.tags);
let names = array_or_empty(prepared.names);
let result = {};
for (let i = 0; i < length(tags); i++)
result[tags[i]] = as_string(names[i] || tags[i]);
return result;
}
function prepared_servers(prepared) {
let tags = array_or_empty(prepared.tags);
let servers = array_or_empty(prepared.servers);
let result = {};
for (let i = 0; i < length(tags); i++) {
let server = as_string(servers[i]);
if (server != "")
result[tags[i]] = server;
}
return result;
}
function prepared_names_lines(prepared) {
for (let name in array_or_empty(prepared.names))
print(as_string(name), "\n");
}
function prepared_slice(start, end) {
let prepared = object_or_empty(read_stdin_json());
start = int(start || 0);
end = int(end || start);
prepared.outbounds = slice(array_or_empty(prepared.outbounds), start, end);
prepared.tags = slice(array_or_empty(prepared.tags), start, end);
prepared.names = slice(array_or_empty(prepared.names), start, end);
prepared.servers = slice(array_or_empty(prepared.servers), start, end);
prepared.links = slice(array_or_empty(prepared.links), start, end);
prepared.source_indices = slice(array_or_empty(prepared.source_indices), start, end);
prepared.skipped = 0;
prepared.skipped_reason_counts = {};
write_json(prepared);
}
function field_json(field) {
let value = object_or_empty(read_stdin_json())[field];
if (field == "outbounds" || field == "tags" || field == "names" || field == "servers" || field == "links" || field == "source_indices")
value = array_or_empty(value);
write_json(value == null ? [] : value);
}
function field_length(field) {
let value = object_or_empty(read_stdin_json())[field];
if (type(value) != "array" && type(value) != "object")
print("0\n");
else
print(length(value), "\n");
}
function tags_csv() {
let prepared = object_or_empty(read_stdin_json());
let tags = [];
for (let tag in array_or_empty(prepared.tags))
push(tags, as_string(tag));
print(join(",", tags), "\n");
}
function metadata_command(kind, source_section) {
let prepared = object_or_empty(read_stdin_json());
if (kind == "link-refs")
write_json(prepared_link_refs(prepared, source_section));
else if (kind == "names")
write_json(prepared_names(prepared));
else if (kind == "servers")
write_json(prepared_servers(prepared));
else if (kind == "names-lines")
prepared_names_lines(prepared);
}
function display_name() {
let prepared = object_or_empty(read_stdin_json());
let names = array_or_empty(prepared.names);
let outbounds = array_or_empty(prepared.outbounds);
let result = as_string(names[0]);
if (result == "" && length(outbounds) > 0 && type(outbounds[0]) == "object")
result = as_string(outbounds[0].tag);
if (result == "")
result = "unknown";
result = replace(result, /[\r\n\t]/g, " ");
result = replace(result, /[ ]+/g, " ");
result = trim(result);
print(result != "" ? result : "unknown", "\n");
}
let mode = ARGV[0] || "";
if (mode == "candidate-outbounds")
candidate_outbounds(ARGV[1]);
else if (mode == "skip-count")
skip_count();
else if (mode == "skip-summary")
skip_summary();
else if (mode == "plugin-names")
plugin_names();
else if (mode == "plugin-supports-from-records")
plugin_supports_from_records(ARGV[1]);
else if (mode == "prepare-subscription")
prepare_subscription(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5]);
else if (mode == "prepare-validation")
prepare_validation(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "prepared-link-refs")
metadata_command("link-refs", as_string(ARGV[1]));
else if (mode == "prepared-names")
metadata_command("names", "");
else if (mode == "prepared-servers")
metadata_command("servers", "");
else if (mode == "prepared-names-lines")
metadata_command("names-lines", "");
else if (mode == "prepared-slice")
prepared_slice(ARGV[1], ARGV[2]);
else if (mode == "prepared-field")
field_json(ARGV[1]);
else if (mode == "prepared-length")
field_length(ARGV[1]);
else if (mode == "prepared-tags-csv")
tags_csv();
else if (mode == "prepared-display-name")
display_name();
else {
warn("Usage: sing_box_config_facade.uc <operation> [args...]\n");
exit(1);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,748 @@
#!/usr/bin/env ucode
let fs = require("fs");
let SERVICE_TAG = "__service_tag";
function as_string(value) {
return value == null ? "" : "" + value;
}
function read_stdin() {
let input = fs.open("/dev/stdin", "r");
if (!input)
return "";
let data = input.read("all");
input.close();
return data == null ? "" : data;
}
function read_config() {
try {
let config = json(read_stdin());
return type(config) == "object" ? config : {};
}
catch (e) {
return {};
}
}
function read_json_file(path) {
let data = fs.readfile(path);
if (data == null)
return null;
try {
return json(data);
}
catch (e) {
return null;
}
}
function write_json(value) {
print(sprintf("%J", value), "\n");
}
function bool_arg(value) {
return as_string(value) == "true";
}
function number_arg(value) {
value = as_string(value);
return value == "" ? null : int(value, 10);
}
function json_arg(value) {
value = as_string(value);
try {
return json(value);
}
catch (e) {
return value;
}
}
function array_arg(value) {
let parsed = json_arg(value);
return type(parsed) == "array" ? parsed : [];
}
function array_file_arg(path) {
let parsed = read_json_file(path);
return type(parsed) == "array" ? parsed : [];
}
function optional_string(object, key, value) {
value = as_string(value);
if (value != "")
object[key] = value;
}
function optional_number(object, key, value) {
let parsed = number_arg(value);
if (parsed != null)
object[key] = parsed;
}
function ensure_object(config, key) {
if (type(config[key]) != "object")
config[key] = {};
return config[key];
}
function ensure_array(object, key) {
if (type(object[key]) != "array")
object[key] = [];
return object[key];
}
function extend_key_value(current_value, new_value) {
let result = [];
if (type(current_value) == "array") {
for (let item in current_value)
push(result, item);
}
else {
push(result, current_value);
}
if (type(new_value) == "array") {
for (let item in new_value)
push(result, item);
}
else {
push(result, new_value);
}
return result;
}
function patch_tagged_rule(rules, tag, key, value) {
for (let rule in rules) {
if (type(rule) == "object" && rule[SERVICE_TAG] == tag) {
if (rule[key] != null)
rule[key] = extend_key_value(rule[key], value);
else
rule[key] = value;
}
}
}
function patch_outbound(config, tag, patch) {
for (let outbound in ensure_array(config, "outbounds")) {
if (type(outbound) == "object" && outbound.tag == tag) {
for (let key, value in patch)
outbound[key] = value;
}
}
}
function add_dns_server(config, server) {
push(ensure_array(ensure_object(config, "dns"), "servers"), server);
}
function add_outbound(config, outbound) {
push(ensure_array(config, "outbounds"), outbound);
}
function add_rule(config, section, rule) {
push(ensure_array(ensure_object(config, section), "rules"), rule);
}
function add_rule_set(config, rule_set) {
push(ensure_array(ensure_object(config, "route"), "rule_set"), rule_set);
}
function clean_service_tags(value) {
if (type(value) == "array") {
for (let i = 0; i < length(value); i++)
value[i] = clean_service_tags(value[i]);
return value;
}
if (type(value) == "object") {
delete value[SERVICE_TAG];
for (let key, item in value)
value[key] = clean_service_tags(item);
}
return value;
}
function configure_log(config, args) {
config.log = {
disabled: bool_arg(args[0]),
level: as_string(args[1]),
timestamp: bool_arg(args[2])
};
}
function configure_dns(config, args) {
let current = type(config.dns) == "object" ? config.dns : {};
config.dns = {
servers: type(current.servers) == "array" ? current.servers : [],
rules: type(current.rules) == "array" ? current.rules : [],
final: as_string(args[0]),
strategy: as_string(args[1]),
independent_cache: bool_arg(args[2])
};
}
function add_udp_dns_server(config, args) {
let server = {
type: "udp",
tag: as_string(args[0]),
server: as_string(args[1]),
server_port: number_arg(args[2])
};
optional_string(server, "domain_resolver", args[3]);
optional_string(server, "detour", args[4]);
add_dns_server(config, server);
}
function add_tls_dns_server(config, args) {
let server = {
type: "tls",
tag: as_string(args[0]),
server: as_string(args[1]),
server_port: number_arg(args[2])
};
optional_string(server, "domain_resolver", args[3]);
optional_string(server, "detour", args[4]);
add_dns_server(config, server);
}
function add_https_dns_server(config, args) {
let server = {
type: "https",
tag: as_string(args[0]),
server: as_string(args[1]),
server_port: number_arg(args[2])
};
optional_string(server, "path", args[3]);
optional_string(server, "headers", args[4]);
optional_string(server, "domain_resolver", args[5]);
optional_string(server, "detour", args[6]);
add_dns_server(config, server);
}
function add_fakeip_dns_server(config, args) {
add_dns_server(config, {
type: "fakeip",
tag: as_string(args[0]),
inet4_range: as_string(args[1])
});
}
function add_dns_route_rule(config, args) {
let rule = {
action: "route",
server: as_string(args[0])
};
rule[SERVICE_TAG] = as_string(args[1]);
add_rule(config, "dns", rule);
}
function patch_dns_route_rule(config, args) {
patch_tagged_rule(ensure_array(ensure_object(config, "dns"), "rules"), as_string(args[0]), as_string(args[1]), json_arg(args[2]));
}
function add_dns_reject_rule(config, args) {
let rule = { action: "reject" };
rule[as_string(args[0])] = json_arg(args[1]);
add_rule(config, "dns", rule);
}
function add_tproxy_inbound(config, args) {
push(ensure_array(config, "inbounds"), {
type: "tproxy",
tag: as_string(args[0]),
listen: as_string(args[1]),
listen_port: number_arg(args[2]),
tcp_fast_open: bool_arg(args[3]),
udp_fragment: bool_arg(args[4])
});
}
function add_direct_inbound(config, args) {
push(ensure_array(config, "inbounds"), {
type: "direct",
tag: as_string(args[0]),
listen: as_string(args[1]),
listen_port: number_arg(args[2])
});
}
function add_mixed_inbound(config, args) {
push(ensure_array(config, "inbounds"), {
type: "mixed",
tag: as_string(args[0]),
listen: as_string(args[1]),
listen_port: number_arg(args[2])
});
}
function add_direct_outbound(config, args) {
let outbound = {
type: "direct",
tag: as_string(args[0])
};
optional_number(outbound, "routing_mark", args[1]);
add_outbound(config, outbound);
}
function add_socks_outbound(config, args) {
let outbound = {
type: "socks",
tag: as_string(args[0]),
server: as_string(args[1]),
server_port: number_arg(args[2])
};
optional_string(outbound, "version", args[3]);
optional_string(outbound, "username", args[4]);
optional_string(outbound, "password", args[5]);
optional_string(outbound, "network", args[6]);
if (as_string(args[7]) != "") {
outbound.udp_over_tcp = {
enabled: true,
version: number_arg(args[7])
};
}
add_outbound(config, outbound);
}
function add_shadowsocks_outbound(config, args) {
let outbound = {
type: "shadowsocks",
tag: as_string(args[0]),
server: as_string(args[1]),
server_port: number_arg(args[2]),
method: as_string(args[3]),
password: as_string(args[4])
};
optional_string(outbound, "network", args[5]);
if (as_string(args[6]) != "") {
outbound.udp_over_tcp = {
enabled: true,
version: number_arg(args[6])
};
}
optional_string(outbound, "plugin", args[7]);
optional_string(outbound, "plugin_opts", args[8]);
add_outbound(config, outbound);
}
function add_vless_outbound(config, args) {
let outbound = {
type: "vless",
tag: as_string(args[0]),
server: as_string(args[1]),
server_port: number_arg(args[2]),
uuid: as_string(args[3])
};
optional_string(outbound, "flow", args[4]);
optional_string(outbound, "network", args[5]);
optional_string(outbound, "packet_encoding", args[6]);
add_outbound(config, outbound);
}
function add_trojan_outbound(config, args) {
let outbound = {
type: "trojan",
tag: as_string(args[0]),
server: as_string(args[1]),
server_port: number_arg(args[2]),
password: as_string(args[3])
};
optional_string(outbound, "network", args[4]);
add_outbound(config, outbound);
}
function add_hysteria2_outbound(config, args) {
let outbound = {
type: "hysteria2",
tag: as_string(args[0]),
server: as_string(args[1]),
server_port: number_arg(args[2]),
password: as_string(args[3])
};
if (as_string(args[4]) != "" && as_string(args[5]) != "") {
outbound.obfs = {
type: as_string(args[4]),
password: as_string(args[5])
};
}
optional_number(outbound, "up_mbps", args[6]);
optional_number(outbound, "down_mbps", args[7]);
optional_string(outbound, "network", args[8]);
add_outbound(config, outbound);
}
function set_grpc_transport(config, args) {
let transport = { type: "grpc" };
optional_string(transport, "service_name", args[1]);
optional_string(transport, "idle_timeout", args[2]);
optional_string(transport, "ping_timeout", args[3]);
optional_string(transport, "permit_without_stream", args[4]);
patch_outbound(config, as_string(args[0]), { transport });
}
function set_ws_transport(config, args) {
let transport = {
type: "ws",
path: as_string(args[1])
};
if (as_string(args[2]) != "")
transport.headers = { Host: as_string(args[2]) };
optional_number(transport, "max_early_data", args[3]);
optional_string(transport, "early_data_header_name", args[4]);
patch_outbound(config, as_string(args[0]), { transport });
}
function set_http_transport(config, args) {
let transport = { type: "http" };
optional_string(transport, "path", args[1]);
let hosts = array_arg(args[2]);
if (length(hosts) > 0)
transport.host = hosts;
patch_outbound(config, as_string(args[0]), { transport });
}
function set_httpupgrade_transport(config, args) {
let transport = { type: "httpupgrade" };
optional_string(transport, "path", args[1]);
optional_string(transport, "host", args[2]);
patch_outbound(config, as_string(args[0]), { transport });
}
function set_xhttp_transport(config, args) {
let mode = as_string(args[3]);
if (mode != "auto" && mode != "packet-up" && mode != "stream-up" && mode != "stream-one")
mode = "auto";
let transport = {
type: "xhttp",
mode,
path: as_string(args[1]) != "" ? as_string(args[1]) : "/",
x_padding_bytes: "100-1000",
no_grpc_header: false,
sc_max_each_post_bytes: 1000000,
sc_min_posts_interval_ms: 30
};
optional_string(transport, "host", args[2]);
patch_outbound(config, as_string(args[0]), { transport });
}
function set_tls(config, args) {
let tls = { enabled: true };
optional_string(tls, "server_name", args[1]);
if (bool_arg(args[2]))
tls.insecure = true;
let alpn = json_arg(args[3]);
if (alpn != null)
tls.alpn = alpn;
if (as_string(args[4]) != "") {
tls.utls = {
enabled: true,
fingerprint: as_string(args[4])
};
}
if (as_string(args[5]) != "") {
tls.reality = {
enabled: true,
public_key: as_string(args[5]),
short_id: as_string(args[6])
};
}
patch_outbound(config, as_string(args[0]), { tls });
}
function add_interface_outbound(config, args) {
let outbound = {
type: "direct",
tag: as_string(args[0]),
bind_interface: as_string(args[1])
};
optional_string(outbound, "domain_resolver", args[2]);
add_outbound(config, outbound);
}
function add_raw_outbound(config, args) {
let outbound = json_arg(args[1]);
if (type(outbound) != "object")
outbound = {};
outbound.tag = as_string(args[0]);
add_outbound(config, outbound);
}
function add_urltest_outbound(config, args) {
let outbound = {
type: "urltest",
tag: as_string(args[0]),
outbounds: array_arg(args[1])
};
optional_string(outbound, "url", args[2]);
optional_string(outbound, "interval", args[3]);
optional_number(outbound, "tolerance", args[4]);
optional_string(outbound, "idle_timeout", args[5]);
if (bool_arg(args[6]))
outbound.interrupt_exist_connections = true;
add_outbound(config, outbound);
}
function add_urltest_outbound_file(config, args) {
let outbound = {
type: "urltest",
tag: as_string(args[0]),
outbounds: array_file_arg(args[1])
};
optional_string(outbound, "url", args[2]);
optional_string(outbound, "interval", args[3]);
optional_number(outbound, "tolerance", args[4]);
optional_string(outbound, "idle_timeout", args[5]);
if (bool_arg(args[6]))
outbound.interrupt_exist_connections = true;
add_outbound(config, outbound);
}
function add_selector_outbound(config, args) {
let outbound = {
type: "selector",
tag: as_string(args[0]),
outbounds: array_arg(args[1]),
default: as_string(args[2])
};
if (bool_arg(args[3]))
outbound.interrupt_exist_connections = true;
add_outbound(config, outbound);
}
function add_selector_outbound_file(config, args) {
let outbound = {
type: "selector",
tag: as_string(args[0]),
outbounds: array_file_arg(args[1]),
default: as_string(args[2])
};
if (bool_arg(args[3]))
outbound.interrupt_exist_connections = true;
add_outbound(config, outbound);
}
function configure_route(config, args) {
let current = type(config.route) == "object" ? config.route : {};
config.route = {
rules: type(current.rules) == "array" ? current.rules : [],
rule_set: type(current.rule_set) == "array" ? current.rule_set : [],
final: as_string(args[0]),
auto_detect_interface: bool_arg(args[1]),
default_domain_resolver: as_string(args[2])
};
optional_string(config.route, "default_interface", args[3]);
}
function add_route_rule(config, args) {
let rule = {
action: "route",
inbound: as_string(args[1]),
outbound: as_string(args[2])
};
rule[SERVICE_TAG] = as_string(args[0]);
add_rule(config, "route", rule);
}
function copy_rule_matchers(rule) {
let copied = {};
for (let key in ["inbound", "source_ip_cidr", "domain", "domain_suffix", "domain_keyword", "domain_regex", "rule_set"]) {
if (rule[key] != null)
copied[key] = rule[key];
}
return copied;
}
function add_resolve_rule(config, args) {
let route = ensure_object(config, "route");
let rules = ensure_array(route, "rules");
let updated = [];
for (let rule in rules) {
if (type(rule) == "object" && rule[SERVICE_TAG] == as_string(args[0])) {
let resolve = copy_rule_matchers(rule);
resolve.action = "resolve";
resolve.server = as_string(args[2]) != "" ? as_string(args[2]) : "dns-server";
resolve[SERVICE_TAG] = as_string(args[1]);
push(updated, resolve);
}
push(updated, rule);
}
route.rules = updated;
}
function patch_route_rule(config, args) {
patch_tagged_rule(ensure_array(ensure_object(config, "route"), "rules"), as_string(args[0]), as_string(args[1]), json_arg(args[2]));
}
function add_reject_route_rule(config, args) {
let rule = {
action: "reject",
inbound: as_string(args[1])
};
rule[SERVICE_TAG] = as_string(args[0]);
add_rule(config, "route", rule);
}
function add_hijack_dns_route_rule(config, args) {
let rule = { action: "hijack-dns" };
rule[as_string(args[0])] = json_arg(args[1]);
add_rule(config, "route", rule);
}
function add_options_route_rule(config, args) {
let rule = { action: "route-options" };
rule[SERVICE_TAG] = as_string(args[0]);
add_rule(config, "route", rule);
}
function sniff_route_rule(config, args) {
let rule = { action: "sniff" };
rule[as_string(args[0])] = json_arg(args[1]);
add_rule(config, "route", rule);
}
function add_inline_ruleset(config, args) {
add_rule_set(config, {
type: "inline",
tag: as_string(args[0])
});
}
function add_inline_ruleset_rule(config, args) {
let rule_sets = ensure_array(ensure_object(config, "route"), "rule_set");
let tag = as_string(args[0]);
let key = as_string(args[1]);
let value = json_arg(args[2]);
for (let rule_set in rule_sets) {
if (type(rule_set) == "object" && rule_set.tag == tag) {
if (rule_set[key] != null)
rule_set[key] = extend_key_value(rule_set[key], value);
else
rule_set[key] = value;
}
}
}
function add_local_ruleset(config, args) {
add_rule_set(config, {
type: "local",
tag: as_string(args[0]),
format: as_string(args[1]),
path: as_string(args[2])
});
}
function add_remote_ruleset(config, args) {
let rule_set = {
type: "remote",
tag: as_string(args[0]),
format: as_string(args[1]),
url: as_string(args[2])
};
optional_string(rule_set, "download_detour", args[3]);
optional_string(rule_set, "update_interval", args[4]);
add_rule_set(config, rule_set);
}
function configure_cache_file(config, args) {
let experimental = ensure_object(config, "experimental");
experimental.cache_file = {
enabled: bool_arg(args[0]),
path: as_string(args[1]),
store_fakeip: bool_arg(args[2])
};
}
function configure_clash_api(config, args) {
let experimental = ensure_object(config, "experimental");
experimental.clash_api = {
external_controller: as_string(args[0])
};
optional_string(experimental.clash_api, "external_ui", args[1]);
optional_string(experimental.clash_api, "secret", args[2]);
}
let handlers = {
"configure-log": configure_log,
"configure-dns": configure_dns,
"add-udp-dns-server": add_udp_dns_server,
"add-tls-dns-server": add_tls_dns_server,
"add-https-dns-server": add_https_dns_server,
"add-fakeip-dns-server": add_fakeip_dns_server,
"add-dns-route-rule": add_dns_route_rule,
"patch-dns-route-rule": patch_dns_route_rule,
"add-dns-reject-rule": add_dns_reject_rule,
"add-tproxy-inbound": add_tproxy_inbound,
"add-direct-inbound": add_direct_inbound,
"add-mixed-inbound": add_mixed_inbound,
"add-direct-outbound": add_direct_outbound,
"add-socks-outbound": add_socks_outbound,
"add-shadowsocks-outbound": add_shadowsocks_outbound,
"add-vless-outbound": add_vless_outbound,
"add-trojan-outbound": add_trojan_outbound,
"add-hysteria2-outbound": add_hysteria2_outbound,
"set-grpc-transport": set_grpc_transport,
"set-ws-transport": set_ws_transport,
"set-http-transport": set_http_transport,
"set-httpupgrade-transport": set_httpupgrade_transport,
"set-xhttp-transport": set_xhttp_transport,
"set-tls": set_tls,
"add-interface-outbound": add_interface_outbound,
"add-raw-outbound": add_raw_outbound,
"add-urltest-outbound": add_urltest_outbound,
"add-urltest-outbound-file": add_urltest_outbound_file,
"add-selector-outbound": add_selector_outbound,
"add-selector-outbound-file": add_selector_outbound_file,
"configure-route": configure_route,
"add-route-rule": add_route_rule,
"add-resolve-rule": add_resolve_rule,
"patch-route-rule": patch_route_rule,
"add-reject-route-rule": add_reject_route_rule,
"add-hijack-dns-route-rule": add_hijack_dns_route_rule,
"add-options-route-rule": add_options_route_rule,
"sniff-route-rule": sniff_route_rule,
"add-inline-ruleset": add_inline_ruleset,
"add-inline-ruleset-rule": add_inline_ruleset_rule,
"add-local-ruleset": add_local_ruleset,
"add-remote-ruleset": add_remote_ruleset,
"configure-cache-file": configure_cache_file,
"configure-clash-api": configure_clash_api
};
let mode = ARGV[0] || "";
if (mode == "normalize-arg") {
write_json(json_arg(ARGV[1]));
exit(0);
}
let config = read_config();
if (mode == "save-config") {
let path = ARGV[1] || "";
if (path == "" || !fs.writefile(path, sprintf("%J", clean_service_tags(config)) + "\n"))
exit(1);
exit(0);
}
let handler = handlers[mode];
if (!handler) {
warn("Usage: sing_box_config_manager.uc <operation> [args...]\n");
exit(1);
}
handler(config, slice(ARGV, 1));
write_json(config);
-828
View File
@@ -1,828 +0,0 @@
#!/usr/bin/env lua
local jsonc_ok, jsonc = pcall(require, "luci.jsonc")
local nixio_ok, nixio = pcall(require, "nixio")
local JSON_ARRAY_MT = { __jsontype = "array" }
local function json_array(value)
return setmetatable(value or {}, JSON_ARRAY_MT)
end
local function is_json_array(value)
return type(value) == "table" and getmetatable(value) == JSON_ARRAY_MT
end
local function json_escape(value)
value = tostring(value or "")
value = value:gsub("\\", "\\\\")
value = value:gsub("\"", "\\\"")
value = value:gsub("\b", "\\b")
value = value:gsub("\f", "\\f")
value = value:gsub("\n", "\\n")
value = value:gsub("\r", "\\r")
value = value:gsub("\t", "\\t")
value = value:gsub("[%z\1-\31]", function(char)
return string.format("\\u%04x", char:byte())
end)
return value
end
local function table_is_array(value)
if is_json_array(value) then
return true
end
local max = 0
local count = 0
for key, _ in pairs(value) do
if type(key) ~= "number" or key < 1 or key % 1 ~= 0 then
return false
end
if key > max then
max = key
end
count = count + 1
end
return count > 0 and max == count
end
local function json_encode(value)
local value_type = type(value)
if value == nil then
return "null"
elseif value_type == "string" then
return "\"" .. json_escape(value) .. "\""
elseif value_type == "number" then
return tostring(value)
elseif value_type == "boolean" then
return value and "true" or "false"
elseif value_type ~= "table" then
return "null"
end
if table_is_array(value) then
local parts = {}
for index = 1, #value do
parts[#parts + 1] = json_encode(value[index])
end
return "[" .. table.concat(parts, ",") .. "]"
end
local keys = {}
for key, _ in pairs(value) do
if type(key) == "string" then
keys[#keys + 1] = key
end
end
table.sort(keys)
local parts = {}
for _, key in ipairs(keys) do
parts[#parts + 1] = "\"" .. json_escape(key) .. "\":" .. json_encode(value[key])
end
return "{" .. table.concat(parts, ",") .. "}"
end
local function read_file(path)
if not path or path == "" or path == "-" then
return nil
end
local file = io.open(path, "rb")
if not file then
return nil
end
local data = file:read("*a")
file:close()
return data
end
local function write_file(path, data)
local file = assert(io.open(path, "wb"))
file:write(data)
file:close()
end
local function json_decode_text(text)
if not text or text == "" then
return nil
end
if jsonc_ok then
return jsonc.parse(text)
end
return nil
end
local function read_json(path)
return json_decode_text(read_file(path))
end
local function is_array(value)
return type(value) == "table" and table_is_array(value)
end
local function has_string_key(value)
if type(value) ~= "table" then
return false
end
for key, _ in pairs(value) do
if type(key) == "string" then
return true
end
end
return false
end
local function object_or_empty(value)
if type(value) ~= "table" then
return {}
end
if is_array(value) and not has_string_key(value) then
return {}
end
return value
end
local function array_or_empty(value)
if type(value) ~= "table" then
return json_array()
end
if is_array(value) then
return value
end
return json_array()
end
local function object_key_count(value)
if type(value) ~= "table" then
return 0
end
local count = 0
for key, _ in pairs(value) do
if type(key) == "string" then
count = count + 1
end
end
return count
end
local function valid_metadata_object(value)
return type(value) == "table" and object_key_count(value) > 1
end
local function safe_section(section)
return type(section) == "string" and section:match("^[A-Za-z0-9_-]+$") ~= nil
end
local function cache_path(cache_dir, section)
return cache_dir .. "/" .. section .. ".json"
end
local function load_cache(cache_dir, section)
local parsed = read_json(cache_path(cache_dir, section))
if type(parsed) == "table" and not is_array(parsed) then
return parsed
end
return {}
end
local function normalize_cache(cache, section, format_version)
cache.version = tonumber(format_version) or format_version
cache.section = section
cache.links = object_or_empty(cache.links)
cache.linkRefs = object_or_empty(cache.linkRefs)
cache.outboundMetadata = object_or_empty(cache.outboundMetadata)
cache.outboundMetadata.names = object_or_empty(cache.outboundMetadata.names)
cache.outboundMetadata.countries = object_or_empty(cache.outboundMetadata.countries)
cache.servers = object_or_empty(cache.servers)
cache.subscriptionMetadata = array_or_empty(cache.subscriptionMetadata)
return cache
end
local function save_cache(cache_dir, section, format_version, cache)
normalize_cache(cache, section, format_version)
local path = cache_path(cache_dir, section)
local tmp_path = path .. "." .. tostring(os.time()) .. "." .. tostring(math.random(1000000)) .. ".tmp"
write_file(tmp_path, json_encode(cache) .. "\n")
assert(os.rename(tmp_path, path))
end
local function write_link_cache(cache_dir, format_version, section, links_path, link_refs_path)
local cache = load_cache(cache_dir, section)
cache.links = object_or_empty(read_json(links_path))
cache.linkRefs = object_or_empty(read_json(link_refs_path))
save_cache(cache_dir, section, format_version, cache)
end
local function write_outbound_metadata(cache_dir, format_version, section, names_path, countries_path, servers_path)
local cache = load_cache(cache_dir, section)
cache.outboundMetadata = {
names = object_or_empty(read_json(names_path)),
countries = object_or_empty(read_json(countries_path))
}
cache.servers = object_or_empty(read_json(servers_path))
save_cache(cache_dir, section, format_version, cache)
end
local function metadata_array_from_file(metadata_path)
local metadata = read_json(metadata_path)
local result = json_array()
if is_array(metadata) then
for _, item in ipairs(metadata) do
if valid_metadata_object(item) then
result[#result + 1] = item
end
end
elseif valid_metadata_object(metadata) then
result[#result + 1] = metadata
end
return result
end
local function write_subscription_metadata(cache_dir, format_version, section, metadata_path)
local cache = load_cache(cache_dir, section)
cache.subscriptionMetadata = metadata_array_from_file(metadata_path)
save_cache(cache_dir, section, format_version, cache)
end
local function read_metadata_items_from_cache(cache_dir, section, legacy_path)
local cache = load_cache(cache_dir, section)
local metadata = cache.subscriptionMetadata
if type(metadata) ~= "table" then
metadata = read_json(legacy_path)
end
local result = json_array()
if is_array(metadata) then
for _, item in ipairs(metadata) do
if valid_metadata_object(item) then
result[#result + 1] = item
end
end
elseif valid_metadata_object(metadata) then
result[#result + 1] = metadata
end
return result
end
local function metadata_source_index(item)
return tonumber(item.sourceIndex or item.source_index)
end
local function metadata_source_section(item)
return tostring(item.sourceSection or item.source_section or "")
end
local function metadata_items_have_source_markers(items)
for _, item in ipairs(items) do
if metadata_source_index(item) ~= nil or metadata_source_section(item) ~= "" then
return true
end
end
return false
end
local function metadata_matches_source(item, source_index, source_section, has_source_markers)
if has_source_markers then
local item_section = metadata_source_section(item)
local item_index = metadata_source_index(item)
return (item_section ~= "" and item_section == source_section) or
(item_section == "" and item_index == source_index)
end
return false
end
local function attach_source_metadata(item, source_index, source_section)
item.sourceIndex = source_index
item.sourceSection = source_section
return item
end
local function append_metadata_file(array_path, metadata_path, source_index, source_section)
if not array_path or array_path == "" then
return
end
local array = array_or_empty(read_json(array_path))
local metadata = read_json(metadata_path)
source_index = tonumber(source_index) or 0
source_section = tostring(source_section or "")
if valid_metadata_object(metadata) then
array[#array + 1] = attach_source_metadata(metadata, source_index, source_section)
write_file(array_path, json_encode(array) .. "\n")
end
end
local function append_cached_metadata(array_path, cache_dir, section, legacy_path, source_index, source_section)
if not array_path or array_path == "" then
return
end
local array = array_or_empty(read_json(array_path))
local items = read_metadata_items_from_cache(cache_dir, section, legacy_path)
source_index = tonumber(source_index) or 0
source_section = tostring(source_section or "")
local has_source_markers = metadata_items_have_source_markers(items)
local selected = nil
if has_source_markers then
for _, item in ipairs(items) do
if metadata_matches_source(item, source_index, source_section, true) then
selected = item
break
end
end
else
selected = items[source_index]
end
if valid_metadata_object(selected) then
array[#array + 1] = attach_source_metadata(selected, source_index, source_section)
write_file(array_path, json_encode(array) .. "\n")
end
end
local function write_source_metadata(cache_dir, format_version, section, source_index, source_section, metadata_path, legacy_path)
local cache = load_cache(cache_dir, section)
local items = read_metadata_items_from_cache(cache_dir, section, legacy_path)
local kept = json_array()
local has_source_markers = metadata_items_have_source_markers(items)
source_index = tonumber(source_index) or 0
source_section = tostring(source_section or "")
for index, item in ipairs(items) do
local keep
if has_source_markers then
keep = not metadata_matches_source(item, source_index, source_section, true)
else
keep = index ~= source_index
end
if keep and valid_metadata_object(item) then
kept[#kept + 1] = item
end
end
local metadata = read_json(metadata_path)
if valid_metadata_object(metadata) then
kept[#kept + 1] = attach_source_metadata(metadata, source_index, source_section)
end
table.sort(kept, function(first, second)
return (metadata_source_index(first) or 999999) < (metadata_source_index(second) or 999999)
end)
cache.subscriptionMetadata = kept
save_cache(cache_dir, section, format_version, cache)
end
local function starts_with(value, prefix)
return value:sub(1, #prefix) == prefix
end
local function uri_encode(value)
return tostring(value or ""):gsub("([^A-Za-z0-9%-%._~])", function(char)
return string.format("%%%02X", char:byte())
end)
end
local function base64_encode(value)
if nixio_ok and nixio.bin and nixio.bin.b64encode then
return (nixio.bin.b64encode(tostring(value or "")):gsub("=+$", ""))
end
return ""
end
local function host_port(server, port)
server = tostring(server or "")
if server:find(":", 1, true) and not starts_with(server, "[") then
server = "[" .. server .. "]"
end
return server .. ":" .. tostring(port or "")
end
local function add_query(params, key, value)
value = tostring(value or "")
if value ~= "" then
params[#params + 1] = uri_encode(key) .. "=" .. uri_encode(value)
end
end
local function add_tls_query(params, outbound, trojan_default_tls)
local tls = type(outbound.tls) == "table" and outbound.tls or nil
if not tls or tls.enabled == false then
if trojan_default_tls then
add_query(params, "security", "tls")
end
return
end
local reality = type(tls.reality) == "table" and tls.reality or nil
if reality and reality.enabled ~= false then
add_query(params, "security", "reality")
add_query(params, "pbk", reality.public_key)
add_query(params, "sid", reality.short_id)
else
add_query(params, "security", "tls")
end
add_query(params, "sni", tls.server_name)
if tls.insecure == true then
add_query(params, "allowInsecure", "1")
end
if type(tls.utls) == "table" and tls.utls.enabled ~= false then
add_query(params, "fp", tls.utls.fingerprint)
end
if type(tls.alpn) == "table" and #tls.alpn > 0 then
add_query(params, "alpn", table.concat(tls.alpn, ","))
end
end
local function add_transport_query(params, outbound)
local transport = type(outbound.transport) == "table" and outbound.transport or nil
if not transport then
add_query(params, "type", "tcp")
return
end
local transport_type = tostring(transport.type or "")
add_query(params, "type", transport_type ~= "" and transport_type or "tcp")
if transport_type == "ws" then
add_query(params, "path", transport.path)
if type(transport.headers) == "table" then
add_query(params, "host", transport.headers.Host or transport.headers.host)
end
elseif transport_type == "grpc" then
add_query(params, "serviceName", transport.service_name)
elseif transport_type == "http" then
add_query(params, "path", transport.path)
if type(transport.host) == "table" and #transport.host > 0 then
add_query(params, "host", table.concat(transport.host, ","))
else
add_query(params, "host", transport.host)
end
elseif transport_type == "xhttp" then
add_query(params, "path", transport.path)
add_query(params, "host", transport.host)
add_query(params, "mode", transport.mode)
end
end
local function query_string(params)
if #params == 0 then
return ""
end
return "?" .. table.concat(params, "&")
end
local function fragment(outbound)
local tag = tostring(outbound.tag or "")
if tag == "" then
return ""
end
return "#" .. uri_encode(tag)
end
local function serialize_vless(outbound)
if tostring(outbound.uuid or "") == "" or tostring(outbound.server or "") == "" or not outbound.server_port then
return ""
end
local params = {}
add_tls_query(params, outbound, false)
add_transport_query(params, outbound)
add_query(params, "flow", outbound.flow)
add_query(params, "packetEncoding", outbound.packet_encoding)
return "vless://" .. uri_encode(outbound.uuid) .. "@" ..
host_port(outbound.server, outbound.server_port) .. query_string(params) .. fragment(outbound)
end
local function serialize_trojan(outbound)
if tostring(outbound.password or "") == "" or tostring(outbound.server or "") == "" or not outbound.server_port then
return ""
end
local params = {}
add_tls_query(params, outbound, true)
add_transport_query(params, outbound)
return "trojan://" .. uri_encode(outbound.password) .. "@" ..
host_port(outbound.server, outbound.server_port) .. query_string(params) .. fragment(outbound)
end
local function serialize_shadowsocks(outbound)
if tostring(outbound.method or "") == "" or tostring(outbound.password or "") == "" or
tostring(outbound.server or "") == "" or not outbound.server_port then
return ""
end
local userinfo = base64_encode(tostring(outbound.method) .. ":" .. tostring(outbound.password))
if userinfo == "" then
return ""
end
return "ss://" .. userinfo .. "@" .. host_port(outbound.server, outbound.server_port) .. fragment(outbound)
end
local function serialize_socks(outbound)
if tostring(outbound.server or "") == "" or not outbound.server_port then
return ""
end
local scheme = "socks" .. tostring(outbound.version or "5")
local auth = ""
if tostring(outbound.username or "") ~= "" then
auth = uri_encode(outbound.username)
if tostring(outbound.password or "") ~= "" then
auth = auth .. ":" .. uri_encode(outbound.password)
end
auth = auth .. "@"
end
return scheme .. "://" .. auth .. host_port(outbound.server, outbound.server_port) .. fragment(outbound)
end
local function serialize_hysteria2(outbound)
if tostring(outbound.password or "") == "" or tostring(outbound.server or "") == "" or not outbound.server_port then
return ""
end
local params = {}
local tls = type(outbound.tls) == "table" and outbound.tls or nil
if tls then
add_query(params, "sni", tls.server_name)
if tls.insecure == true then
add_query(params, "insecure", "1")
end
if type(tls.alpn) == "table" and #tls.alpn > 0 then
add_query(params, "alpn", table.concat(tls.alpn, ","))
end
end
if type(outbound.obfs) == "table" then
add_query(params, "obfs", outbound.obfs.type)
add_query(params, "obfs-password", outbound.obfs.password)
end
return "hysteria2://" .. uri_encode(outbound.password) .. "@" ..
host_port(outbound.server, outbound.server_port) .. query_string(params) .. fragment(outbound)
end
local function serialize_vmess(outbound)
if tostring(outbound.uuid or "") == "" or tostring(outbound.server or "") == "" or not outbound.server_port then
return ""
end
local vmess = {
v = "2",
ps = tostring(outbound.tag or ""),
add = tostring(outbound.server or ""),
port = tostring(outbound.server_port or ""),
id = tostring(outbound.uuid or ""),
aid = tostring(outbound.alter_id or 0),
scy = tostring(outbound.security or "auto"),
net = "tcp",
type = "none",
host = "",
path = "",
tls = "",
sni = ""
}
if type(outbound.tls) == "table" and outbound.tls.enabled ~= false then
vmess.tls = "tls"
vmess.sni = tostring(outbound.tls.server_name or "")
if type(outbound.tls.utls) == "table" then
vmess.fp = tostring(outbound.tls.utls.fingerprint or "")
end
end
if type(outbound.transport) == "table" then
vmess.net = tostring(outbound.transport.type or "tcp")
if vmess.net == "ws" then
vmess.path = tostring(outbound.transport.path or "")
if type(outbound.transport.headers) == "table" then
vmess.host = tostring(outbound.transport.headers.Host or outbound.transport.headers.host or "")
end
elseif vmess.net == "grpc" then
vmess.path = tostring(outbound.transport.service_name or "")
elseif vmess.net == "http" then
vmess.path = tostring(outbound.transport.path or "")
if type(outbound.transport.host) == "table" and #outbound.transport.host > 0 then
vmess.host = table.concat(outbound.transport.host, ",")
else
vmess.host = tostring(outbound.transport.host or "")
end
end
end
local encoded = base64_encode(json_encode(vmess))
if encoded == "" then
return ""
end
return "vmess://" .. encoded
end
local function serialize_outbound_link(outbound)
if type(outbound) ~= "table" then
return ""
end
local outbound_type = tostring(outbound.type or "")
if outbound_type == "vless" then
return serialize_vless(outbound)
elseif outbound_type == "trojan" then
return serialize_trojan(outbound)
elseif outbound_type == "shadowsocks" then
return serialize_shadowsocks(outbound)
elseif outbound_type == "socks" then
return serialize_socks(outbound)
elseif outbound_type == "hysteria2" then
return serialize_hysteria2(outbound)
elseif outbound_type == "vmess" then
return serialize_vmess(outbound)
end
return ""
end
local function is_copyable_link(value)
value = tostring(value or ""):lower()
local prefixes = {
"vless://", "vmess://", "trojan://", "ss://", "ssr://",
"hysteria2://", "hy2://", "tuic://",
"socks4://", "socks4a://", "socks5://"
}
for _, prefix in ipairs(prefixes) do
if starts_with(value, prefix) then
return true
end
end
return false
end
local function get_source_link(subscription_dir, ref)
if type(ref) ~= "table" then
return ""
end
local source_section = tostring(ref.sourceSection or ref.source_section or "")
local source_index = tonumber(ref.sourceIndex or ref.source_index)
if not safe_section(source_section) or not source_index or source_index < 1 then
return ""
end
local source = read_json(subscription_dir .. "/" .. source_section .. ".json")
if type(source) ~= "table" or type(source.outbounds) ~= "table" then
return ""
end
local outbound = source.outbounds[source_index]
if type(outbound) ~= "table" then
return ""
end
local link = tostring(outbound.share_link or "")
if link == "" then
link = serialize_outbound_link(outbound)
end
if is_copyable_link(link) then
return link
end
return ""
end
local function get_link(cache_dir, subscription_dir, section, tag, legacy_links_dir)
local cache = load_cache(cache_dir, section)
local links = object_or_empty(cache.links)
local link_refs = object_or_empty(cache.linkRefs)
local link = tostring(links[tag] or "")
if not is_copyable_link(link) then
link = get_source_link(subscription_dir, link_refs[tag])
end
if not is_copyable_link(link) and legacy_links_dir and legacy_links_dir ~= "" then
local legacy = object_or_empty(read_json(legacy_links_dir .. "/" .. section .. ".json"))
link = tostring(legacy[tag] or "")
end
if not is_copyable_link(link) then
link = ""
end
print(json_encode({ link = link }))
end
local function get_link_states(cache_dir, section, legacy_links_dir)
local cache = load_cache(cache_dir, section)
local result = {}
for tag, link in pairs(object_or_empty(cache.links)) do
result[tag] = is_copyable_link(link)
end
for tag, _ in pairs(object_or_empty(cache.linkRefs)) do
result[tag] = true
end
if next(result) == nil and legacy_links_dir and legacy_links_dir ~= "" then
local legacy = object_or_empty(read_json(legacy_links_dir .. "/" .. section .. ".json"))
for tag, link in pairs(legacy) do
result[tag] = is_copyable_link(link)
end
end
print(json_encode(result))
end
local function get_outbound_metadata(cache_dir, section, legacy_path)
local cache = load_cache(cache_dir, section)
local metadata = cache.outboundMetadata
if type(metadata) ~= "table" then
metadata = read_json(legacy_path)
end
metadata = object_or_empty(metadata)
print(json_encode({
names = object_or_empty(metadata.names),
countries = object_or_empty(metadata.countries)
}))
end
local function get_subscription_metadata(cache_dir, section, legacy_path)
local items = read_metadata_items_from_cache(cache_dir, section, legacy_path)
if #items > 0 then
print(json_encode(items))
else
print("{}")
end
end
local mode = arg[1] or ""
if mode == "write-link-cache" then
local cache_dir, format_version, section = arg[2], arg[3], arg[4]
if not safe_section(section) then os.exit(1) end
write_link_cache(cache_dir, format_version, section, arg[5], arg[6])
elseif mode == "write-outbound-metadata" then
local cache_dir, format_version, section = arg[2], arg[3], arg[4]
if not safe_section(section) then os.exit(1) end
write_outbound_metadata(cache_dir, format_version, section, arg[5], arg[6], arg[7])
elseif mode == "write-subscription-metadata" then
local cache_dir, format_version, section = arg[2], arg[3], arg[4]
if not safe_section(section) then os.exit(1) end
write_subscription_metadata(cache_dir, format_version, section, arg[5])
elseif mode == "append-metadata-file" then
append_metadata_file(arg[2], arg[3], arg[4], arg[5])
elseif mode == "append-cached-metadata" then
append_cached_metadata(arg[2], arg[3], arg[4], arg[5], arg[6], arg[7])
elseif mode == "write-source-metadata" then
local cache_dir, format_version, section = arg[2], arg[3], arg[4]
if not safe_section(section) then os.exit(1) end
write_source_metadata(cache_dir, format_version, section, arg[5], arg[6], arg[7], arg[8])
elseif mode == "get-link" then
local cache_dir, subscription_dir, section = arg[2], arg[3], arg[4]
if not safe_section(section) then
print("{\"link\":\"\"}")
else
get_link(cache_dir, subscription_dir, section, arg[5] or "", arg[6] or "")
end
elseif mode == "get-link-states" then
local cache_dir, section = arg[2], arg[3]
if not safe_section(section) then
print("{}")
else
get_link_states(cache_dir, section, arg[4] or "")
end
elseif mode == "get-outbound-metadata" then
local cache_dir, section = arg[2], arg[3]
if not safe_section(section) then
print("{\"names\":{},\"countries\":{}}")
else
get_outbound_metadata(cache_dir, section, arg[4])
end
elseif mode == "get-subscription-metadata" then
local cache_dir, section = arg[2], arg[3]
if not safe_section(section) then
print("{}")
else
get_subscription_metadata(cache_dir, section, arg[4])
end
else
io.stderr:write("Usage: subscription_cache.lua <mode> ...\n")
os.exit(1)
end
+681
View File
@@ -0,0 +1,681 @@
#!/usr/bin/env ucode
let fs = require("fs");
function as_string(value) {
return value == null ? "" : "" + value;
}
function read_json(path) {
if (path == null || path == "" || path == "-")
return null;
let data = fs.readfile(path);
if (data == null)
return null;
try {
return json(data);
}
catch (e) {
return null;
}
}
function write_file(path, value) {
return fs.writefile(path, value);
}
function write_json(path, value) {
return write_file(path, sprintf("%J", value) + "\n");
}
function write_stdout_json(value) {
print(sprintf("%J", value), "\n");
}
function is_array(value) {
return type(value) == "array";
}
function object_or_empty(value) {
return type(value) == "object" ? value : {};
}
function array_or_empty(value) {
return type(value) == "array" ? value : [];
}
function object_key_count(value) {
return type(value) == "object" ? length(keys(value)) : 0;
}
function valid_metadata_object(value) {
return type(value) == "object" && object_key_count(value) > 1;
}
function safe_section(section) {
return type(section) == "string" && match(section, /^[A-Za-z0-9_-]+$/);
}
function cache_path(cache_dir, section) {
return cache_dir + "/" + section + ".json";
}
function load_cache(cache_dir, section) {
return object_or_empty(read_json(cache_path(cache_dir, section)));
}
function normalize_cache(cache, section, format_version) {
cache.version = int(format_version || 0);
cache.section = as_string(section);
cache.links = object_or_empty(cache.links);
cache.linkRefs = object_or_empty(cache.linkRefs);
cache.outboundMetadata = object_or_empty(cache.outboundMetadata);
cache.outboundMetadata.names = object_or_empty(cache.outboundMetadata.names);
cache.outboundMetadata.countries = object_or_empty(cache.outboundMetadata.countries);
cache.servers = object_or_empty(cache.servers);
cache.subscriptionMetadata = array_or_empty(cache.subscriptionMetadata);
return cache;
}
function save_cache(cache_dir, section, format_version, cache) {
cache = normalize_cache(cache, section, format_version);
let path = cache_path(cache_dir, section);
let stamp = clock();
let tmp_path = sprintf("%s.%d.%d.tmp", path, stamp[0], stamp[1]);
if (!write_json(tmp_path, cache))
exit(1);
if (!fs.rename(tmp_path, path)) {
fs.unlink(tmp_path);
exit(1);
}
}
function write_link_cache(cache_dir, format_version, section, links_path, link_refs_path) {
let cache = load_cache(cache_dir, section);
cache.links = object_or_empty(read_json(links_path));
cache.linkRefs = object_or_empty(read_json(link_refs_path));
save_cache(cache_dir, section, format_version, cache);
}
function write_outbound_metadata(cache_dir, format_version, section, names_path, countries_path, servers_path) {
let cache = load_cache(cache_dir, section);
cache.outboundMetadata = {
names: object_or_empty(read_json(names_path)),
countries: object_or_empty(read_json(countries_path))
};
cache.servers = object_or_empty(read_json(servers_path));
save_cache(cache_dir, section, format_version, cache);
}
function metadata_array_from_file(metadata_path) {
let metadata = read_json(metadata_path);
let result = [];
if (is_array(metadata)) {
for (let item in metadata) {
if (valid_metadata_object(item))
push(result, item);
}
}
else if (valid_metadata_object(metadata)) {
push(result, metadata);
}
return result;
}
function write_subscription_metadata(cache_dir, format_version, section, metadata_path) {
let cache = load_cache(cache_dir, section);
cache.subscriptionMetadata = metadata_array_from_file(metadata_path);
save_cache(cache_dir, section, format_version, cache);
}
function read_metadata_items_from_cache(cache_dir, section, legacy_path) {
let cache = load_cache(cache_dir, section);
let metadata = cache.subscriptionMetadata;
if (type(metadata) != "array")
metadata = read_json(legacy_path);
let result = [];
if (is_array(metadata)) {
for (let item in metadata) {
if (valid_metadata_object(item))
push(result, item);
}
}
else if (valid_metadata_object(metadata)) {
push(result, metadata);
}
return result;
}
function metadata_source_index(item) {
if (type(item) != "object")
return null;
let value = item.sourceIndex != null ? item.sourceIndex : item.source_index;
if (value == null || as_string(value) == "")
return null;
return int(value);
}
function metadata_source_section(item) {
if (type(item) != "object")
return "";
return as_string(item.sourceSection != null ? item.sourceSection : item.source_section);
}
function metadata_items_have_source_markers(items) {
for (let item in items) {
if (metadata_source_index(item) != null || metadata_source_section(item) != "")
return true;
}
return false;
}
function metadata_matches_source(item, source_index, source_section, has_source_markers) {
if (!has_source_markers)
return false;
let item_section = metadata_source_section(item);
let item_index = metadata_source_index(item);
return (item_section != "" && item_section == source_section) ||
(item_section == "" && item_index == source_index);
}
function attach_source_metadata(item, source_index, source_section) {
if (type(item) != "object")
item = {};
item.sourceIndex = source_index;
item.sourceSection = as_string(source_section);
return item;
}
function append_metadata_file(array_path, metadata_path, source_index, source_section) {
if (array_path == null || array_path == "")
return;
let array = array_or_empty(read_json(array_path));
let metadata = read_json(metadata_path);
source_index = int(source_index || 0);
source_section = as_string(source_section);
if (valid_metadata_object(metadata)) {
push(array, attach_source_metadata(metadata, source_index, source_section));
write_json(array_path, array);
}
}
function append_cached_metadata(array_path, cache_dir, section, legacy_path, source_index, source_section) {
if (array_path == null || array_path == "")
return;
let array = array_or_empty(read_json(array_path));
let items = read_metadata_items_from_cache(cache_dir, section, legacy_path);
source_index = int(source_index || 0);
source_section = as_string(source_section);
let has_source_markers = metadata_items_have_source_markers(items);
let selected = null;
if (has_source_markers) {
for (let item in items) {
if (metadata_matches_source(item, source_index, source_section, true)) {
selected = item;
break;
}
}
}
else if (source_index > 0 && source_index <= length(items)) {
selected = items[source_index - 1];
}
if (valid_metadata_object(selected)) {
push(array, attach_source_metadata(selected, source_index, source_section));
write_json(array_path, array);
}
}
function write_source_metadata(cache_dir, format_version, section, source_index, source_section, metadata_path, legacy_path) {
let cache = load_cache(cache_dir, section);
let items = read_metadata_items_from_cache(cache_dir, section, legacy_path);
let kept = [];
let has_source_markers = metadata_items_have_source_markers(items);
source_index = int(source_index || 0);
source_section = as_string(source_section);
for (let index, item in items) {
let keep = has_source_markers ?
!metadata_matches_source(item, source_index, source_section, true) :
(index + 1) != source_index;
if (keep && valid_metadata_object(item))
push(kept, item);
}
let metadata = read_json(metadata_path);
if (valid_metadata_object(metadata))
push(kept, attach_source_metadata(metadata, source_index, source_section));
sort(kept, function(first, second) {
let first_index = metadata_source_index(first) || 999999;
let second_index = metadata_source_index(second) || 999999;
return first_index == second_index ? 0 : (first_index < second_index ? -1 : 1);
});
cache.subscriptionMetadata = kept;
save_cache(cache_dir, section, format_version, cache);
}
function starts_with(value, prefix) {
value = as_string(value);
prefix = as_string(prefix);
return substr(value, 0, length(prefix)) == prefix;
}
function uri_encode(value) {
value = as_string(value);
let result = "";
for (let i = 0; i < length(value); i++) {
let char = substr(value, i, 1);
let code = ord(char);
if ((code >= 48 && code <= 57) ||
(code >= 65 && code <= 90) ||
(code >= 97 && code <= 122) ||
char == "-" || char == "_" || char == "." || char == "~")
result += char;
else
result += sprintf("%%%02X", code);
}
return result;
}
function base64_encode(value) {
let encoded = b64enc(as_string(value));
while (length(encoded) > 0 && substr(encoded, length(encoded) - 1) == "=")
encoded = substr(encoded, 0, length(encoded) - 1);
return encoded;
}
function host_port(server, port) {
server = as_string(server);
if (index(server, ":") >= 0 && !starts_with(server, "["))
server = "[" + server + "]";
return server + ":" + as_string(port);
}
function add_query(params, key, value) {
value = as_string(value);
if (value != "")
push(params, uri_encode(key) + "=" + uri_encode(value));
}
function add_tls_query(params, outbound, trojan_default_tls) {
let tls = type(outbound.tls) == "object" ? outbound.tls : null;
if (!tls || tls.enabled === false) {
if (trojan_default_tls)
add_query(params, "security", "tls");
return;
}
let reality = type(tls.reality) == "object" ? tls.reality : null;
if (reality && reality.enabled !== false) {
add_query(params, "security", "reality");
add_query(params, "pbk", reality.public_key);
add_query(params, "sid", reality.short_id);
}
else {
add_query(params, "security", "tls");
}
add_query(params, "sni", tls.server_name);
if (tls.insecure === true)
add_query(params, "allowInsecure", "1");
if (type(tls.utls) == "object" && tls.utls.enabled !== false)
add_query(params, "fp", tls.utls.fingerprint);
if (type(tls.alpn) == "array" && length(tls.alpn) > 0)
add_query(params, "alpn", join(",", tls.alpn));
}
function add_transport_query(params, outbound) {
let transport = type(outbound.transport) == "object" ? outbound.transport : null;
if (!transport) {
add_query(params, "type", "tcp");
return;
}
let transport_type = as_string(transport.type);
add_query(params, "type", transport_type != "" ? transport_type : "tcp");
if (transport_type == "ws") {
add_query(params, "path", transport.path);
if (type(transport.headers) == "object")
add_query(params, "host", transport.headers.Host || transport.headers.host);
}
else if (transport_type == "grpc") {
add_query(params, "serviceName", transport.service_name);
}
else if (transport_type == "http") {
add_query(params, "path", transport.path);
if (type(transport.host) == "array" && length(transport.host) > 0)
add_query(params, "host", join(",", transport.host));
else
add_query(params, "host", transport.host);
}
else if (transport_type == "xhttp") {
add_query(params, "path", transport.path);
add_query(params, "host", transport.host);
add_query(params, "mode", transport.mode);
}
}
function query_string(params) {
return length(params) == 0 ? "" : "?" + join("&", params);
}
function fragment(outbound) {
let tag = as_string(outbound.tag);
return tag == "" ? "" : "#" + uri_encode(tag);
}
function serialize_vless(outbound) {
if (as_string(outbound.uuid) == "" || as_string(outbound.server) == "" || outbound.server_port == null)
return "";
let params = [];
add_tls_query(params, outbound, false);
add_transport_query(params, outbound);
add_query(params, "flow", outbound.flow);
add_query(params, "packetEncoding", outbound.packet_encoding);
return "vless://" + uri_encode(outbound.uuid) + "@" +
host_port(outbound.server, outbound.server_port) + query_string(params) + fragment(outbound);
}
function serialize_trojan(outbound) {
if (as_string(outbound.password) == "" || as_string(outbound.server) == "" || outbound.server_port == null)
return "";
let params = [];
add_tls_query(params, outbound, true);
add_transport_query(params, outbound);
return "trojan://" + uri_encode(outbound.password) + "@" +
host_port(outbound.server, outbound.server_port) + query_string(params) + fragment(outbound);
}
function serialize_shadowsocks(outbound) {
if (as_string(outbound.method) == "" || as_string(outbound.password) == "" ||
as_string(outbound.server) == "" || outbound.server_port == null)
return "";
let userinfo = base64_encode(as_string(outbound.method) + ":" + as_string(outbound.password));
return userinfo == "" ? "" :
"ss://" + userinfo + "@" + host_port(outbound.server, outbound.server_port) + fragment(outbound);
}
function serialize_socks(outbound) {
if (as_string(outbound.server) == "" || outbound.server_port == null)
return "";
let scheme = "socks" + as_string(outbound.version || "5");
let auth = "";
if (as_string(outbound.username) != "") {
auth = uri_encode(outbound.username);
if (as_string(outbound.password) != "")
auth += ":" + uri_encode(outbound.password);
auth += "@";
}
return scheme + "://" + auth + host_port(outbound.server, outbound.server_port) + fragment(outbound);
}
function serialize_hysteria2(outbound) {
if (as_string(outbound.password) == "" || as_string(outbound.server) == "" || outbound.server_port == null)
return "";
let params = [];
let tls = type(outbound.tls) == "object" ? outbound.tls : null;
if (tls) {
add_query(params, "sni", tls.server_name);
if (tls.insecure === true)
add_query(params, "insecure", "1");
if (type(tls.alpn) == "array" && length(tls.alpn) > 0)
add_query(params, "alpn", join(",", tls.alpn));
}
if (type(outbound.obfs) == "object") {
add_query(params, "obfs", outbound.obfs.type);
add_query(params, "obfs-password", outbound.obfs.password);
}
return "hysteria2://" + uri_encode(outbound.password) + "@" +
host_port(outbound.server, outbound.server_port) + query_string(params) + fragment(outbound);
}
function serialize_vmess(outbound) {
if (as_string(outbound.uuid) == "" || as_string(outbound.server) == "" || outbound.server_port == null)
return "";
let vmess = {
v: "2",
ps: as_string(outbound.tag),
add: as_string(outbound.server),
port: as_string(outbound.server_port),
id: as_string(outbound.uuid),
aid: as_string(outbound.alter_id || 0),
scy: as_string(outbound.security || "auto"),
net: "tcp",
type: "none",
host: "",
path: "",
tls: "",
sni: ""
};
if (type(outbound.tls) == "object" && outbound.tls.enabled !== false) {
vmess.tls = "tls";
vmess.sni = as_string(outbound.tls.server_name);
if (type(outbound.tls.utls) == "object")
vmess.fp = as_string(outbound.tls.utls.fingerprint);
}
if (type(outbound.transport) == "object") {
vmess.net = as_string(outbound.transport.type || "tcp");
if (vmess.net == "ws") {
vmess.path = as_string(outbound.transport.path);
if (type(outbound.transport.headers) == "object")
vmess.host = as_string(outbound.transport.headers.Host || outbound.transport.headers.host);
}
else if (vmess.net == "grpc") {
vmess.path = as_string(outbound.transport.service_name);
}
else if (vmess.net == "http") {
vmess.path = as_string(outbound.transport.path);
if (type(outbound.transport.host) == "array" && length(outbound.transport.host) > 0)
vmess.host = join(",", outbound.transport.host);
else
vmess.host = as_string(outbound.transport.host);
}
}
let encoded = base64_encode(sprintf("%J", vmess));
return encoded == "" ? "" : "vmess://" + encoded;
}
function serialize_outbound_link(outbound) {
if (type(outbound) != "object")
return "";
let outbound_type = as_string(outbound.type);
if (outbound_type == "vless")
return serialize_vless(outbound);
if (outbound_type == "trojan")
return serialize_trojan(outbound);
if (outbound_type == "shadowsocks")
return serialize_shadowsocks(outbound);
if (outbound_type == "socks")
return serialize_socks(outbound);
if (outbound_type == "hysteria2")
return serialize_hysteria2(outbound);
if (outbound_type == "vmess")
return serialize_vmess(outbound);
return "";
}
function is_copyable_link(value) {
value = lc(as_string(value));
let prefixes = [
"vless://", "vmess://", "trojan://", "ss://", "ssr://",
"hysteria2://", "hy2://", "tuic://",
"socks4://", "socks4a://", "socks5://"
];
for (let prefix in prefixes) {
if (starts_with(value, prefix))
return true;
}
return false;
}
function get_source_link(subscription_dir, ref) {
if (type(ref) != "object")
return "";
let source_section = as_string(ref.sourceSection || ref.source_section);
let source_index = int(ref.sourceIndex || ref.source_index || 0);
if (!safe_section(source_section) || source_index < 1)
return "";
let source = read_json(subscription_dir + "/" + source_section + ".json");
if (type(source) != "object" || type(source.outbounds) != "array")
return "";
let outbound = source.outbounds[source_index - 1];
if (type(outbound) != "object")
return "";
let link = as_string(outbound.share_link);
if (link == "")
link = serialize_outbound_link(outbound);
return is_copyable_link(link) ? link : "";
}
function get_link(cache_dir, subscription_dir, section, tag, legacy_links_dir) {
let cache = load_cache(cache_dir, section);
let links = object_or_empty(cache.links);
let link_refs = object_or_empty(cache.linkRefs);
let link = as_string(links[tag]);
if (!is_copyable_link(link))
link = get_source_link(subscription_dir, link_refs[tag]);
if (!is_copyable_link(link) && legacy_links_dir != null && legacy_links_dir != "") {
let legacy = object_or_empty(read_json(legacy_links_dir + "/" + section + ".json"));
link = as_string(legacy[tag]);
}
if (!is_copyable_link(link))
link = "";
write_stdout_json({ link: link });
}
function get_link_states(cache_dir, section, legacy_links_dir) {
let cache = load_cache(cache_dir, section);
let result = {};
for (let tag, link in object_or_empty(cache.links))
result[tag] = is_copyable_link(link);
for (let tag, _ in object_or_empty(cache.linkRefs))
result[tag] = true;
if (length(keys(result)) == 0 && legacy_links_dir != null && legacy_links_dir != "") {
let legacy = object_or_empty(read_json(legacy_links_dir + "/" + section + ".json"));
for (let tag, link in legacy)
result[tag] = is_copyable_link(link);
}
write_stdout_json(result);
}
function get_outbound_metadata(cache_dir, section, legacy_path) {
let cache = load_cache(cache_dir, section);
let metadata = cache.outboundMetadata;
if (type(metadata) != "object")
metadata = read_json(legacy_path);
metadata = object_or_empty(metadata);
write_stdout_json({
names: object_or_empty(metadata.names),
countries: object_or_empty(metadata.countries)
});
}
function get_subscription_metadata(cache_dir, section, legacy_path) {
let items = read_metadata_items_from_cache(cache_dir, section, legacy_path);
if (length(items) > 0)
write_stdout_json(items);
else
print("{}\n");
}
let mode = ARGV[0] || "";
if (mode == "write-link-cache") {
let cache_dir = ARGV[1], format_version = ARGV[2], section = ARGV[3];
if (!safe_section(section))
exit(1);
write_link_cache(cache_dir, format_version, section, ARGV[4], ARGV[5]);
}
else if (mode == "write-outbound-metadata") {
let cache_dir = ARGV[1], format_version = ARGV[2], section = ARGV[3];
if (!safe_section(section))
exit(1);
write_outbound_metadata(cache_dir, format_version, section, ARGV[4], ARGV[5], ARGV[6]);
}
else if (mode == "write-subscription-metadata") {
let cache_dir = ARGV[1], format_version = ARGV[2], section = ARGV[3];
if (!safe_section(section))
exit(1);
write_subscription_metadata(cache_dir, format_version, section, ARGV[4]);
}
else if (mode == "append-metadata-file") {
append_metadata_file(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
}
else if (mode == "append-cached-metadata") {
append_cached_metadata(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], ARGV[6]);
}
else if (mode == "write-source-metadata") {
let cache_dir = ARGV[1], format_version = ARGV[2], section = ARGV[3];
if (!safe_section(section))
exit(1);
write_source_metadata(cache_dir, format_version, section, ARGV[4], ARGV[5], ARGV[6], ARGV[7]);
}
else if (mode == "get-link") {
let cache_dir = ARGV[1], subscription_dir = ARGV[2], section = ARGV[3];
if (!safe_section(section))
print("{\"link\":\"\"}\n");
else
get_link(cache_dir, subscription_dir, section, ARGV[4] || "", ARGV[5] || "");
}
else if (mode == "get-link-states") {
let cache_dir = ARGV[1], section = ARGV[2];
if (!safe_section(section))
print("{}\n");
else
get_link_states(cache_dir, section, ARGV[3] || "");
}
else if (mode == "get-outbound-metadata") {
let cache_dir = ARGV[1], section = ARGV[2];
if (!safe_section(section))
print("{\"names\":{},\"countries\":{}}\n");
else
get_outbound_metadata(cache_dir, section, ARGV[3]);
}
else if (mode == "get-subscription-metadata") {
let cache_dir = ARGV[1], section = ARGV[2];
if (!safe_section(section))
print("{}\n");
else
get_subscription_metadata(cache_dir, section, ARGV[3]);
}
else {
warn("Usage: subscription_cache.uc <mode> ...\n");
exit(1);
}
File diff suppressed because it is too large Load Diff
+62 -704
View File
@@ -1,13 +1,5 @@
# shellcheck shell=ash
subscription_content_has_share_links() {
grep -Eq '^[[:space:]]*(ss|vmess|vless|trojan|hysteria2|hy2|socks|socks4|socks4a|socks5)://' "$1"
}
subscription_content_is_clash_yaml() {
grep -Eq '^[[:space:]]*proxies:[[:space:]]*($|#)' "$1"
}
subscription_url_decode() {
case "$1" in
*%* | *+*) url_decode "$1" ;;
@@ -24,35 +16,6 @@ subscription_url_get_fragment() {
esac
}
subscription_bool_is_true() {
case "$1" in
1 | true | TRUE | True | yes | YES | on | ON)
return 0
;;
esac
return 1
}
subscription_bool_json() {
if subscription_bool_is_true "$1"; then
printf 'true'
else
printf 'false'
fi
}
subscription_normalize_utls_fingerprint() {
case "$1" in
"" | chrome | firefox | edge | safari | 360 | ios | android | randomized | randomizedalpn | randomizednoalpn)
printf '%s' "$1"
;;
*)
printf 'chrome'
;;
esac
}
subscription_base64_decode_string() {
local raw="$1"
local normalized remainder padding
@@ -350,7 +313,7 @@ subscription_metadata_raw_value() {
local raw_json="$1"
local key="$2"
printf '%s\n' "$raw_json" | jq -r --arg key "$key" '.[$key] // empty' 2>/dev/null
printf '%s\n' "$raw_json" | subscription_json_utils_ucode object-get "$key" 2>/dev/null
}
subscription_normalize_ui_metadata_json() {
@@ -413,64 +376,60 @@ EOF
remaining_json="$remaining"
fi
jq -cn \
--arg title "$title" \
--arg webPageUrl "$web_page_url" \
--arg supportUrl "$support_url" \
--arg announce "$announce" \
--arg announceUrl "$announce_url" \
--arg fileName "$file_name" \
--argjson hasTraffic "$has_traffic" \
--argjson upload "$upload_json" \
--argjson download "$download_json" \
--argjson used "$used" \
--argjson total "$total_json" \
--argjson remaining "$remaining_json" \
--argjson isUnlimited "$is_unlimited" \
--argjson expire "$expire_json" \
--argjson refillDate "$refill_date_json" '
{
version: 1
}
+ (if $title != "" then {title: $title} else {} end)
+ (if $hasTraffic then
{
traffic: (
{}
+ (if $upload != null then {upload: $upload} else {} end)
+ (if $download != null then {download: $download} else {} end)
+ {used: $used, isUnlimited: $isUnlimited}
+ (if $total != null and $total > 0 then {total: $total, remaining: $remaining} else {} end)
)
}
else {} end)
+ (if $expire != null and $expire > 0 then {expire: $expire} else {} end)
+ (if $refillDate != null and $refillDate > 0 then {refillDate: $refillDate} else {} end)
+ (if $webPageUrl != "" then {webPageUrl: $webPageUrl} else {} end)
+ (if $supportUrl != "" then {supportUrl: $supportUrl} else {} end)
+ (if $announce != "" then {announce: $announce} else {} end)
+ (if $announceUrl != "" then {announceUrl: $announceUrl} else {} end)
+ (if $fileName != "" then {fileName: $fileName} else {} end)
| if (keys | length) > 1 then . else empty end
'
subscription_json_utils_ucode subscription-ui-metadata \
"$title" \
"$web_page_url" \
"$support_url" \
"$announce" \
"$announce_url" \
"$file_name" \
"$has_traffic" \
"$upload_json" \
"$download_json" \
"$used" \
"$total_json" \
"$remaining_json" \
"$is_unlimited" \
"$expire_json" \
"$refill_date_json"
}
subscription_extract_ui_metadata() {
local headers_file="$1"
local body_file="$2"
local output="$3"
local headers_json body_json raw_json metadata_json
local headers_json body_json raw_json metadata_json headers_tmp body_tmp status
headers_json="$(subscription_metadata_headers_json "$headers_file")" || headers_json="{}"
body_json="$(subscription_metadata_body_json "$body_file")" || body_json="{}"
raw_json="$(jq -cn --argjson body "$body_json" --argjson headers "$headers_json" '$body + $headers')" || {
body_tmp="$(mktemp)" || {
rm -f "$output"
return 1
}
headers_tmp="$(mktemp)" || {
rm -f "$body_tmp" "$output"
return 1
}
printf '%s' "$body_json" > "$body_tmp" || {
rm -f "$body_tmp" "$headers_tmp" "$output"
return 1
}
printf '%s' "$headers_json" > "$headers_tmp" || {
rm -f "$body_tmp" "$headers_tmp" "$output"
return 1
}
raw_json="$(subscription_json_utils_ucode objects-merge "$body_tmp" "$headers_tmp" 2>/dev/null)"
status=$?
rm -f "$body_tmp" "$headers_tmp"
if [ "$status" -ne 0 ] || [ -z "$raw_json" ]; then
rm -f "$output"
return 1
fi
metadata_json="$(subscription_normalize_ui_metadata_json "$raw_json")" || metadata_json=""
if [ -n "$metadata_json" ] && printf '%s\n' "$metadata_json" | jq -e 'type == "object" and (keys | length > 1)' >/dev/null 2>&1; then
if [ -n "$metadata_json" ]; then
printf '%s\n' "$metadata_json" > "$output"
return 0
fi
@@ -479,653 +438,52 @@ subscription_extract_ui_metadata() {
return 1
}
subscription_strip_metadata_preamble_file() {
local input="$1"
local tmpfile
subscription_parser_ucode() {
ucode "${PODKOP_LIB:-/usr/lib/podkop-plus}/subscription_parser.uc" "$@"
}
[ -s "$input" ] || return 0
tmpfile="$(mktemp)" || return 0
awk '
function allowed(k) {
return k == "profile-title" ||
k == "subscription-userinfo" ||
k == "profile-web-page-url" ||
k == "support-url" ||
k == "announce" ||
k == "announce-url" ||
k == "subscription-refill-date" ||
k == "content-disposition"
}
NR <= 20 {
line = $0
sub(/\r$/, "", line)
gsub(/^[ \t]+|[ \t]+$/, "", line)
if (line ~ /^(#|\/\/)[ \t]*[A-Za-z0-9][A-Za-z0-9_-]*[ \t]*:/) {
key = line
sub(/^(#|\/\/)[ \t]*/, "", key)
sub(/[ \t]*:.*/, "", key)
key = tolower(key)
if (!allowed(key)) {
print
next
}
next
}
}
{
print
}
' "$input" > "$tmpfile" && mv "$tmpfile" "$input" || rm -f "$tmpfile"
subscription_json_utils_ucode() {
ucode "${PODKOP_LIB:-/usr/lib/podkop-plus}/json_utils.uc" "$@"
}
subscription_validate_normalized_file() {
local output="$1"
jq -e '
type == "object" and
(.outbounds | type == "array") and
((.outbounds | length) > 0)
' "$output" >/dev/null 2>&1
subscription_json_utils_ucode validate-subscription "$output" >/dev/null 2>&1
}
subscription_normalize_sing_box_json_file() {
local input="$1"
local output="$2"
subscription_log_normalized_skipped() {
local output="$1"
local skipped format message
jq -ce '
def candidate_outbounds:
if type == "object" and (.outbounds | type == "array") then
.outbounds
elif type == "array" then
.
elif type == "object" and (.type? | type == "string") then
[.]
else
[]
end;
skipped="$(subscription_json_utils_ucode json-file-field "$output" "skipped" "0" 2>/dev/null)"
case "$skipped" in
'' | *[!0-9]*) skipped=0 ;;
esac
[ "$skipped" -gt 0 ] || return 0
{
version: 1,
format: "sing-box-json",
outbounds: (candidate_outbounds | map(select(type == "object")))
}
' "$input" > "$output" 2>/dev/null
}
subscription_normalize_uri_list_file() {
local input="$1"
local output="$2"
subscription_normalize_uri_list_file_lua "$input" "$output"
}
subscription_normalize_uri_list_file_lua() {
local input="$1"
local output="$2"
local parser_path="${PODKOP_LIB:-/usr/lib/podkop-plus}/subscription_parser.lua"
local tmp_output skipped
command -v lua >/dev/null 2>&1 || {
log "Lua interpreter is required to parse URI subscription links" "error"
return 1
}
[ -r "$parser_path" ] || {
log "Lua subscription parser not found: $parser_path" "error"
return 1
}
tmp_output="$(mktemp)" || return 1
if lua "$parser_path" normalize-uri-list "$input" "$tmp_output"; then
skipped="$(jq -r '.skipped // 0' "$tmp_output" 2>/dev/null)"
case "$skipped" in
'' | *[!0-9]*) skipped=0 ;;
esac
if [ "$skipped" -gt 0 ]; then
log "Skipped $skipped invalid or unsupported subscription links" "warn"
fi
mv "$tmp_output" "$output"
return 0
fi
rm -f "$tmp_output"
log "Lua subscription parser failed" "error"
return 1
}
subscription_normalize_clash_yaml_file_lua() {
local input="$1"
local output="$2"
local parser_path="${PODKOP_LIB:-/usr/lib/podkop-plus}/subscription_parser.lua"
local tmp_output skipped
command -v lua >/dev/null 2>&1 || {
log "Lua interpreter is required to parse Clash YAML subscriptions" "error"
return 1
}
[ -r "$parser_path" ] || {
log "Lua subscription parser not found: $parser_path" "error"
return 1
}
tmp_output="$(mktemp)" || return 1
if lua "$parser_path" normalize-clash-yaml "$input" "$tmp_output"; then
skipped="$(jq -r '.skipped // 0' "$tmp_output" 2>/dev/null)"
case "$skipped" in
'' | *[!0-9]*) skipped=0 ;;
esac
if [ "$skipped" -gt 0 ]; then
log "Skipped $skipped invalid or unsupported Clash proxy entries" "warn"
fi
mv "$tmp_output" "$output"
return 0
fi
rm -f "$tmp_output"
log "Lua Clash YAML parser failed" "error"
return 1
}
subscription_clash_yaml_records() {
local input="$1"
awk '
function trim(s) {
gsub(/^[ \t\r\n]+/, "", s)
gsub(/[ \t\r\n]+$/, "", s)
return s
}
function json_escape(s) {
gsub(/\\/, "\\\\", s)
gsub(/"/, "\\\"", s)
gsub(/\t/, "\\t", s)
gsub(/\r/, "", s)
return s
}
function clean_scalar(s) {
s = trim(s)
if ((substr(s, 1, 1) == "\"" && substr(s, length(s), 1) == "\"") ||
(substr(s, 1, 1) == "'"'"'" && substr(s, length(s), 1) == "'"'"'")) {
s = substr(s, 2, length(s) - 2)
}
return s
}
function clear_fields( k) {
for (k in fields) {
delete fields[k]
}
field_count = 0
ctx = ""
ctx_indent = -1
}
function set_field(k, v) {
k = clean_scalar(k)
v = clean_scalar(v)
if (k == "") {
return
}
if (!(k in fields)) {
keys[++field_count] = k
}
fields[k] = v
}
function emit_record( i,k,sep) {
if (field_count <= 0) {
return
}
printf "{"
sep = ""
for (i = 1; i <= field_count; i++) {
k = keys[i]
if (k in fields) {
printf "%s\"%s\":\"%s\"", sep, json_escape(k), json_escape(fields[k])
sep = ","
}
}
printf "}\n"
clear_fields()
}
function parse_pair(part, prefix, i,c,depth,quote,esc,colon,key,value) {
part = trim(part)
depth = 0
quote = ""
esc = 0
colon = 0
for (i = 1; i <= length(part); i++) {
c = substr(part, i, 1)
if (quote != "") {
if (esc) {
esc = 0
} else if (c == "\\") {
esc = 1
} else if (c == quote) {
quote = ""
}
continue
}
if (c == "\"" || c == "'"'"'") {
quote = c
continue
}
if (c == "{" || c == "[") {
depth++
} else if (c == "}" || c == "]") {
depth--
} else if (c == ":" && depth == 0) {
colon = i
break
}
}
if (colon <= 0) {
return
}
key = clean_scalar(substr(part, 1, colon - 1))
value = trim(substr(part, colon + 1))
if (substr(value, 1, 1) == "{" && substr(value, length(value), 1) == "}") {
parse_map(value, prefix key ".")
} else {
set_field(prefix key, value)
}
}
function parse_map(s, prefix, i,c,depth,quote,esc,start,part) {
s = trim(s)
if (substr(s, 1, 1) == "{") {
s = substr(s, 2)
}
if (substr(s, length(s), 1) == "}") {
s = substr(s, 1, length(s) - 1)
}
depth = 0
quote = ""
esc = 0
start = 1
for (i = 1; i <= length(s); i++) {
c = substr(s, i, 1)
if (quote != "") {
if (esc) {
esc = 0
} else if (c == "\\") {
esc = 1
} else if (c == quote) {
quote = ""
}
continue
}
if (c == "\"" || c == "'"'"'") {
quote = c
continue
}
if (c == "{" || c == "[") {
depth++
} else if (c == "}" || c == "]") {
depth--
} else if (c == "," && depth == 0) {
part = substr(s, start, i - start)
parse_pair(part, prefix)
start = i + 1
}
}
part = substr(s, start)
parse_pair(part, prefix)
}
function leading_indent(s, i,c) {
for (i = 1; i <= length(s); i++) {
c = substr(s, i, 1)
if (c != " " && c != "\t") {
return i - 1
}
}
return length(s)
}
function parse_block_line(line, indent,t,key,value,colon,fullkey) {
indent = leading_indent(line)
t = trim(line)
if (t == "") {
return
}
colon = index(t, ":")
if (colon <= 0) {
return
}
key = clean_scalar(substr(t, 1, colon - 1))
value = trim(substr(t, colon + 1))
if (ctx != "" && indent <= ctx_indent) {
ctx = ""
ctx_indent = -1
}
if (value == "") {
if (key == "ws-opts" || key == "grpc-opts" || key == "reality-opts" || key == "obfs-opts" || key == "headers") {
if (ctx != "" && indent > ctx_indent) {
ctx = ctx "." key
} else {
ctx = key
}
ctx_indent = indent
}
return
}
fullkey = key
if (ctx != "" && indent > ctx_indent) {
fullkey = ctx "." key
}
set_field(fullkey, value)
}
BEGIN {
in_proxies = 0
clear_fields()
}
/^[^ \t]/ {
if (in_proxies && $0 !~ /^proxies:[ \t]*($|#)/ && $0 !~ /^-[ \t]*/) {
emit_record()
in_proxies = 0
}
}
/^[ \t]*proxies:[ \t]*($|#)/ {
emit_record()
in_proxies = 1
next
}
{
if (!in_proxies) {
next
}
line = $0
if (line ~ /^[ \t]*-[ \t]*\{/) {
emit_record()
clear_fields()
sub(/^[ \t]*-[ \t]*/, "", line)
parse_map(line, "")
emit_record()
next
}
if (line ~ /^[ \t]*-[ \t]*/) {
emit_record()
clear_fields()
sub(/^[ \t]*-[ \t]*/, "", line)
if (trim(line) != "") {
parse_block_line(line)
}
next
}
if (field_count > 0) {
parse_block_line(line)
}
}
END {
emit_record()
}
' "$input"
}
subscription_clash_value() {
local record="$1"
local key="$2"
local rest value
case "$record" in
*\"$key\":\"*)
rest="${record#*\"$key\":\"}"
value="${rest%%\"*}"
value="$(printf '%s' "$value" | sed 's/\\"/"/g;s/\\\\/\\/g')"
printf '%s\n' "$value"
format="$(subscription_json_utils_ucode json-file-field "$output" "format" "" 2>/dev/null)"
case "$format" in
clash-yaml)
message="Skipped $skipped invalid or unsupported Clash proxy entries"
;;
*)
printf '\n'
message="Skipped $skipped invalid or unsupported subscription links"
;;
esac
}
subscription_parse_clash_record() {
local record="$1"
local type name server port tls skip_verify sni network ws_path ws_host grpc_service_name \
reality_public_key reality_short_id alpn fingerprint password uuid cipher alter_id flow packet_encoding \
obfs obfs_password username
type="$(subscription_clash_value "$record" "type" | tr '[:upper:]' '[:lower:]')"
name="$(subscription_clash_value "$record" "name")"
server="$(subscription_clash_value "$record" "server")"
port="$(subscription_clash_value "$record" "port")"
tls="$(subscription_clash_value "$record" "tls")"
skip_verify="$(subscription_clash_value "$record" "skip-cert-verify")"
sni="$(subscription_clash_value "$record" "sni")"
[ -n "$sni" ] || sni="$(subscription_clash_value "$record" "servername")"
network="$(subscription_clash_value "$record" "network" | tr '[:upper:]' '[:lower:]')"
ws_path="$(subscription_clash_value "$record" "ws-opts.path")"
ws_host="$(subscription_clash_value "$record" "ws-opts.headers.Host")"
grpc_service_name="$(subscription_clash_value "$record" "grpc-opts.grpc-service-name")"
reality_public_key="$(subscription_clash_value "$record" "reality-opts.public-key")"
reality_short_id="$(subscription_clash_value "$record" "reality-opts.short-id")"
alpn="$(subscription_clash_value "$record" "alpn" | sed 's/^\[//;s/\]$//;s/[[:space:]]//g')"
fingerprint="$(subscription_clash_value "$record" "client-fingerprint")"
[ -n "$fingerprint" ] || fingerprint="$(subscription_clash_value "$record" "fingerprint")"
fingerprint="$(subscription_normalize_utls_fingerprint "$fingerprint")"
[ -n "$name" ] || name="$server:$port"
[ -n "$type" ] && [ -n "$server" ] && [ -n "$port" ] || return 1
case "$type" in
ss | shadowsocks)
cipher="$(subscription_clash_value "$record" "cipher")"
password="$(subscription_clash_value "$record" "password")"
[ -n "$cipher" ] && [ -n "$password" ] || return 1
[ "$cipher" = "ss" ] && return 1
jq -cn --arg tag "$name" --arg server "$server" --arg port "$port" --arg method "$cipher" --arg password "$password" '
{type:"shadowsocks", tag:$tag, server:$server, server_port:($port|tonumber), method:$method, password:$password}'
;;
vmess)
uuid="$(subscription_clash_value "$record" "uuid")"
alter_id="$(subscription_clash_value "$record" "alterId")"
[ -n "$alter_id" ] || alter_id="$(subscription_clash_value "$record" "alter-id")"
cipher="$(subscription_clash_value "$record" "cipher")"
[ -n "$cipher" ] || cipher="auto"
[ -n "$uuid" ] || return 1
jq -cn \
--arg type "vmess" --arg tag "$name" --arg server "$server" --arg port "$port" \
--arg uuid "$uuid" --arg alter_id "$alter_id" --arg security "$cipher" \
--arg tls "$(subscription_bool_json "$tls")" \
--arg skip_verify "$(subscription_bool_json "$skip_verify")" \
--arg sni "$sni" --arg alpn "$alpn" --arg fingerprint "$fingerprint" \
--arg network "$network" --arg ws_path "$ws_path" --arg ws_host "$ws_host" --arg grpc_service_name "$grpc_service_name" '
{
type:$type, tag:$tag, server:$server, server_port:($port|tonumber), uuid:$uuid, security:$security
}
+ (if $alter_id != "" then {alter_id:($alter_id|tonumber)} else {} end)
+ (if $tls == "true" or $sni != "" or $fingerprint != "" then
{tls: ({enabled:true}
+ (if $sni != "" then {server_name:$sni} else {} end)
+ (if $skip_verify == "true" then {insecure:true} else {} end)
+ (if $alpn != "" then {alpn:($alpn|split(","))} else {} end)
+ (if $fingerprint != "" then {utls:{enabled:true,fingerprint:$fingerprint}} else {} end))}
else {} end)
+ (if $network == "ws" then {transport:({type:"ws", path:(if $ws_path != "" then $ws_path else "/" end)} + (if $ws_host != "" then {headers:{Host:$ws_host}} else {} end))}
elif $network == "grpc" then {transport:({type:"grpc"} + (if $grpc_service_name != "" then {service_name:$grpc_service_name} else {} end))}
else {} end)'
;;
vless)
uuid="$(subscription_clash_value "$record" "uuid")"
flow="$(subscription_clash_value "$record" "flow")"
packet_encoding="$(subscription_clash_value "$record" "packet-encoding")"
[ -n "$packet_encoding" ] || packet_encoding="$(subscription_clash_value "$record" "packetEncoding")"
packet_encoding="$(subscription_normalize_packet_encoding "$packet_encoding")"
[ -n "$uuid" ] || return 1
subscription_vless_flow_is_supported "$flow" || return 1
jq -cn \
--arg tag "$name" --arg server "$server" --arg port "$port" --arg uuid "$uuid" \
--arg flow "$flow" --arg packet_encoding "$packet_encoding" \
--arg tls "$(subscription_bool_json "$tls")" \
--arg skip_verify "$(subscription_bool_json "$skip_verify")" \
--arg sni "$sni" --arg alpn "$alpn" --arg fingerprint "$fingerprint" \
--arg reality_public_key "$reality_public_key" --arg reality_short_id "$reality_short_id" \
--arg network "$network" --arg ws_path "$ws_path" --arg ws_host "$ws_host" --arg grpc_service_name "$grpc_service_name" '
{
type:"vless", tag:$tag, server:$server, server_port:($port|tonumber), uuid:$uuid
}
+ (if $flow != "" then {flow:$flow} else {} end)
+ (if $packet_encoding != "" then {packet_encoding:$packet_encoding} else {} end)
+ (if $tls == "true" or $sni != "" or $fingerprint != "" or $reality_public_key != "" then
{tls: ({enabled:true}
+ (if $sni != "" then {server_name:$sni} else {} end)
+ (if $skip_verify == "true" then {insecure:true} else {} end)
+ (if $alpn != "" then {alpn:($alpn|split(","))} else {} end)
+ (if $reality_public_key != "" then
{utls:{enabled:true,fingerprint:(if $fingerprint != "" then $fingerprint else "chrome" end)}}
elif $fingerprint != "" then
{utls:{enabled:true,fingerprint:$fingerprint}}
else {} end)
+ (if $reality_public_key != "" then {reality:({enabled:true, public_key:$reality_public_key} + (if $reality_short_id != "" then {short_id:$reality_short_id} else {} end))} else {} end))}
else {} end)
+ (if $network == "ws" then {transport:({type:"ws", path:(if $ws_path != "" then $ws_path else "/" end)} + (if $ws_host != "" then {headers:{Host:$ws_host}} else {} end))}
elif $network == "grpc" then {transport:({type:"grpc"} + (if $grpc_service_name != "" then {service_name:$grpc_service_name} else {} end))}
else {} end)'
;;
trojan)
password="$(subscription_clash_value "$record" "password")"
[ -n "$password" ] || return 1
jq -cn \
--arg tag "$name" --arg server "$server" --arg port "$port" --arg password "$password" \
--arg tls "$(subscription_bool_json "$tls")" \
--arg skip_verify "$(subscription_bool_json "$skip_verify")" \
--arg sni "$sni" --arg alpn "$alpn" --arg fingerprint "$fingerprint" \
--arg reality_public_key "$reality_public_key" --arg reality_short_id "$reality_short_id" \
--arg network "$network" --arg ws_path "$ws_path" --arg ws_host "$ws_host" --arg grpc_service_name "$grpc_service_name" '
{
type:"trojan", tag:$tag, server:$server, server_port:($port|tonumber), password:$password
}
+ {tls: ({enabled:true}
+ (if $sni != "" then {server_name:$sni} else {} end)
+ (if $skip_verify == "true" then {insecure:true} else {} end)
+ (if $alpn != "" then {alpn:($alpn|split(","))} else {} end)
+ (if $reality_public_key != "" then
{utls:{enabled:true,fingerprint:(if $fingerprint != "" then $fingerprint else "chrome" end)}}
elif $fingerprint != "" then
{utls:{enabled:true,fingerprint:$fingerprint}}
else {} end)
+ (if $reality_public_key != "" then {reality:({enabled:true, public_key:$reality_public_key} + (if $reality_short_id != "" then {short_id:$reality_short_id} else {} end))} else {} end))}
+ (if $network == "ws" then {transport:({type:"ws", path:(if $ws_path != "" then $ws_path else "/" end)} + (if $ws_host != "" then {headers:{Host:$ws_host}} else {} end))}
elif $network == "grpc" then {transport:({type:"grpc"} + (if $grpc_service_name != "" then {service_name:$grpc_service_name} else {} end))}
else {} end)'
;;
hysteria2 | hy2)
password="$(subscription_clash_value "$record" "password")"
obfs="$(subscription_clash_value "$record" "obfs")"
obfs_password="$(subscription_clash_value "$record" "obfs-password")"
[ -n "$obfs_password" ] || obfs_password="$(subscription_clash_value "$record" "obfs-opts.password")"
[ -n "$password" ] || return 1
jq -cn \
--arg tag "$name" --arg server "$server" --arg port "$port" --arg password "$password" \
--arg skip_verify "$(subscription_bool_json "$skip_verify")" \
--arg sni "$sni" --arg alpn "$alpn" --arg obfs "$obfs" --arg obfs_password "$obfs_password" '
{
type:"hysteria2", tag:$tag, server:$server, server_port:($port|tonumber), password:$password,
tls: ({enabled:true}
+ (if $sni != "" then {server_name:$sni} else {} end)
+ (if $skip_verify == "true" then {insecure:true} else {} end)
+ (if $alpn != "" then {alpn:($alpn|split(","))} else {} end))
}
+ (if $obfs != "" and $obfs != "none" then {obfs:({type:$obfs} + (if $obfs_password != "" then {password:$obfs_password} else {} end))} else {} end)'
;;
socks5 | socks)
username="$(subscription_clash_value "$record" "username")"
password="$(subscription_clash_value "$record" "password")"
jq -cn --arg tag "$name" --arg server "$server" --arg port "$port" --arg username "$username" --arg password "$password" '
{type:"socks", tag:$tag, server:$server, server_port:($port|tonumber), version:"5"}
+ (if $username != "" then {username:$username} else {} end)
+ (if $password != "" then {password:$password} else {} end)'
;;
*)
return 1
;;
esac
}
subscription_normalize_clash_yaml_file() {
local input="$1"
local output="$2"
local records jsonl record added skipped
if subscription_normalize_clash_yaml_file_lua "$input" "$output"; then
return 0
fi
return 1
records="$(mktemp)" || return 1
jsonl="$(mktemp)" || {
rm -f "$records"
return 1
}
subscription_clash_yaml_records "$input" > "$records"
added=0
skipped=0
while IFS= read -r record || [ -n "$record" ]; do
[ -n "$record" ] || continue
if subscription_parse_clash_record "$record" >> "$jsonl"; then
added=$((added + 1))
else
skipped=$((skipped + 1))
log "Skip unsupported or invalid Clash proxy entry" "warn"
fi
done < "$records"
rm -f "$records"
if [ "$added" -eq 0 ]; then
rm -f "$jsonl"
return 1
fi
jq -s --argjson skipped "$skipped" '{
version: 1,
format: "clash-yaml",
skipped: $skipped,
outbounds: .
}' "$jsonl" > "$output"
rm -f "$jsonl"
log "$message" "warn"
}
subscription_normalize_content_file() {
local input="$1"
local output="$2"
local depth="${3:-0}"
local decoded_tmp
convert_crlf_to_lf "$input"
subscription_strip_metadata_preamble_file "$input"
if jq -e . "$input" >/dev/null 2>&1; then
subscription_normalize_sing_box_json_file "$input" "$output" || return 1
if subscription_parser_ucode normalize-content "$input" "$output"; then
subscription_log_normalized_skipped "$output"
subscription_validate_normalized_file "$output"
return $?
fi
if subscription_content_is_clash_yaml "$input"; then
subscription_normalize_clash_yaml_file "$input" "$output" || return 1
subscription_validate_normalized_file "$output"
return $?
fi
if subscription_content_has_share_links "$input"; then
subscription_normalize_uri_list_file "$input" "$output" || return 1
subscription_validate_normalized_file "$output"
return $?
fi
if [ "$depth" -lt 1 ]; then
decoded_tmp="$(mktemp)" || return 1
if subscription_try_base64_decode_file "$input" "$decoded_tmp"; then
if subscription_normalize_content_file "$decoded_tmp" "$output" $((depth + 1)); then
rm -f "$decoded_tmp"
return 0
fi
fi
rm -f "$decoded_tmp"
fi
return 1
}
File diff suppressed because it is too large Load Diff
+83 -210
View File
@@ -42,6 +42,21 @@ updates_log() {
log "Updates: $message" "$level"
}
updates_json_file_get_default() {
local json_file="$1"
local key="$2"
local fallback="$3"
json_utils_ucode json-file-field "$json_file" "$key" "$fallback" 2>/dev/null
}
updates_json_file_running_is() {
local json_file="$1"
local expected="$2"
json_utils_ucode job-running-is "$json_file" "$expected" >/dev/null 2>&1
}
updates_init_tmp_dir() {
[ -n "$UPDATES_TMP_DIR" ] && return 0
@@ -103,25 +118,15 @@ updates_json_response() {
local changed="${7:-0}"
local status="${8:-}"
jq -cn \
--argjson success "$success" \
--arg component "$component" \
--arg action "$action" \
--arg message "$message" \
--arg current_version "$current_version" \
--arg latest_version "$latest_version" \
--argjson changed "$changed" \
--arg status "$status" \
'{
success: $success,
component: $component,
action: $action,
message: $message,
current_version: $current_version,
latest_version: $latest_version,
changed: $changed,
status: $status
}'
json_utils_ucode object-json \
success b "$success" \
component s "$component" \
action s "$action" \
message s "$message" \
current_version s "$current_version" \
latest_version s "$latest_version" \
changed n "$changed" \
status s "$status"
}
updates_success() {
@@ -146,15 +151,10 @@ updates_job_json_response() {
local job_id="$2"
local message="${3:-}"
jq -cn \
--argjson success "$success" \
--arg job_id "$job_id" \
--arg message "$message" \
'{
success: $success,
job_id: $job_id,
message: $message
}'
json_utils_ucode object-json \
success b "$success" \
job_id s "$job_id" \
message s "$message"
}
updates_job_state_path() {
@@ -194,7 +194,7 @@ updates_cleanup_component_jobs() {
if [ -f "$state_file" ]; then
updates_refresh_running_job_state "$state_file"
if jq -e '(.running // false) == true' "$state_file" >/dev/null 2>&1; then
if updates_json_file_running_is "$state_file" true; then
continue
fi
fi
@@ -207,7 +207,7 @@ updates_cleanup_component_jobs() {
find "$UPDATES_JOB_DIR" -type f -name '*.json' -mmin "+$UPDATES_JOB_FINISHED_TTL_MINUTES" 2>/dev/null |
while IFS= read -r state_file; do
[ -f "$state_file" ] || continue
if jq -e '(.running // false) == false' "$state_file" >/dev/null 2>&1; then
if updates_json_file_running_is "$state_file" false; then
rm -f "$state_file" 2>/dev/null || true
fi
done
@@ -228,28 +228,35 @@ updates_write_running_job_state() {
mkdir -p "$UPDATES_JOB_DIR" || return 1
tmp_file="$(updates_job_tmp_file "$state_file")" || return 1
jq -cn \
--argjson success true \
--argjson running true \
--arg component "$component" \
--arg action "$action" \
--arg message "Component action is running" \
--arg pid "$pid" \
--argjson started_at "$started_at" \
'{
success: $success,
running: $running,
component: $component,
action: $action,
message: $message,
pid: (if $pid == "" then null else $pid end),
started_at: $started_at,
current_version: "",
latest_version: "",
changed: 0,
status: "",
exit_code: null
}' >"$tmp_file" && mv "$tmp_file" "$state_file"
if [ -n "$pid" ]; then
json_utils_ucode object-json \
success b true \
running b true \
component s "$component" \
action s "$action" \
message s "Component action is running" \
pid s "$pid" \
started_at n "$started_at" \
current_version s "" \
latest_version s "" \
changed n 0 \
status s "" \
exit_code j null >"$tmp_file" && mv "$tmp_file" "$state_file"
else
json_utils_ucode object-json \
success b true \
running b true \
component s "$component" \
action s "$action" \
message s "Component action is running" \
pid j null \
started_at n "$started_at" \
current_version s "" \
latest_version s "" \
changed n 0 \
status s "" \
exit_code j null >"$tmp_file" && mv "$tmp_file" "$state_file"
fi
local rc=$?
rm -f "$tmp_file" 2>/dev/null
@@ -266,10 +273,7 @@ updates_update_running_job_pid() {
esac
tmp_file="$(updates_job_tmp_file "$state_file")" || return 1
jq -c \
--arg pid "$pid" \
'if (.running // false) == true then . + {pid: $pid} else . end' \
"$state_file" >"$tmp_file" && mv "$tmp_file" "$state_file"
json_utils_ucode updates-set-running-job-pid "$state_file" "$pid" >"$tmp_file" && mv "$tmp_file" "$state_file"
local rc=$?
rm -f "$tmp_file" 2>/dev/null
@@ -281,23 +285,7 @@ updates_mark_stale_job_state() {
local tmp_file
tmp_file="$(updates_job_tmp_file "$state_file")" || return 1
jq -c \
--argjson success false \
--argjson running false \
--arg message "Component action job is stale or the worker process exited unexpectedly" \
'if (.running // false) == true then
. + {
success: $success,
running: $running,
message: $message,
changed: 0,
status: "",
exit_code: null
}
else
.
end' \
"$state_file" >"$tmp_file" && mv "$tmp_file" "$state_file"
json_utils_ucode updates-mark-stale-job-state "$state_file" >"$tmp_file" && mv "$tmp_file" "$state_file"
local rc=$?
rm -f "$tmp_file" 2>/dev/null
@@ -326,10 +314,10 @@ updates_refresh_running_job_state() {
local state_file="$1"
local pid started_at
jq -e '(.running // false) == true' "$state_file" >/dev/null 2>&1 || return 0
updates_json_file_running_is "$state_file" true || return 0
pid="$(jq -r '.pid // empty' "$state_file" 2>/dev/null)"
started_at="$(jq -r '.started_at // 0' "$state_file" 2>/dev/null)"
pid="$(updates_json_file_get_default "$state_file" pid "")"
started_at="$(updates_json_file_get_default "$state_file" started_at 0)"
case "$pid" in
"" | *[!0-9]*)
updates_mark_stale_job_state "$state_file"
@@ -342,7 +330,7 @@ updates_refresh_running_job_state() {
fi
updates_started_at_is_within_stale_grace "$started_at" && return 0
jq -e '(.running // false) == true' "$state_file" >/dev/null 2>&1 || return 0
updates_json_file_running_is "$state_file" true || return 0
updates_mark_stale_job_state "$state_file"
}
@@ -362,25 +350,15 @@ updates_write_finished_job_state() {
"" | *[!0-9]*) updated_at=0 ;;
esac
if jq -e . "$output_file" >/dev/null 2>&1; then
jq -c \
--argjson running false \
--argjson exit_code "$exit_code" \
--argjson updated_at "$updated_at" \
'. + {running: $running, exit_code: $exit_code, updated_at: $updated_at}' \
"$output_file" >"$tmp_file" && mv "$tmp_file" "$state_file"
if json_utils_ucode file-json-valid "$output_file" >/dev/null 2>&1; then
json_utils_ucode updates-finish-job-state "$output_file" "$exit_code" "$updated_at" >"$tmp_file" && mv "$tmp_file" "$state_file"
rm -f "$tmp_file" "$output_file"
return 0
fi
sed -n 's/^[^{]*\({.*\)$/\1/p' "$output_file" 2>/dev/null | tail -n 1 >"$json_file"
if [ -s "$json_file" ] && jq -e . "$json_file" >/dev/null 2>&1; then
jq -c \
--argjson running false \
--argjson exit_code "$exit_code" \
--argjson updated_at "$updated_at" \
'. + {running: $running, exit_code: $exit_code, updated_at: $updated_at}' \
"$json_file" >"$tmp_file" && mv "$tmp_file" "$state_file"
if [ -s "$json_file" ] && json_utils_ucode file-json-valid "$json_file" >/dev/null 2>&1; then
json_utils_ucode updates-finish-job-state "$json_file" "$exit_code" "$updated_at" >"$tmp_file" && mv "$tmp_file" "$state_file"
rm -f "$tmp_file" "$json_file" "$output_file"
return 0
fi
@@ -389,27 +367,7 @@ updates_write_finished_job_state() {
raw_output="$(tr '\n' ' ' <"$output_file" 2>/dev/null | cut -c1-240)"
[ -n "$raw_output" ] || raw_output="Failed to execute"
jq -cn \
--argjson success false \
--argjson running false \
--arg component "$component" \
--arg action "$action" \
--arg message "$raw_output" \
--argjson exit_code "$exit_code" \
--argjson updated_at "$updated_at" \
'{
success: $success,
running: $running,
component: $component,
action: $action,
message: $message,
current_version: "",
latest_version: "",
changed: 0,
status: "",
exit_code: $exit_code,
updated_at: $updated_at
}' >"$tmp_file" && mv "$tmp_file" "$state_file"
json_utils_ucode updates-fallback-job-state "$component" "$action" "$raw_output" "$exit_code" "$updated_at" >"$tmp_file" && mv "$tmp_file" "$state_file"
rm -f "$tmp_file" "$output_file"
}
@@ -930,18 +888,11 @@ updates_resolve_arch_candidates() {
updates_fetch_github_release_json() {
local owner="$1"
local repo="$2"
local response message
local response
response="$(updates_http_get "https://api.github.com/repos/${owner}/${repo}/releases/latest" 2>/dev/null || true)"
[ -n "$response" ] || return 1
printf '%s' "$response" | jq -e . >/dev/null 2>&1 || return 1
message="$(printf '%s' "$response" | jq -r 'if type == "object" then (.message // empty) else empty end' 2>/dev/null)"
case "$message" in
*"API rate limit"* | *"rate limit exceeded"* | "Not Found")
return 1
;;
esac
printf '%s' "$response" | json_utils_ucode github-response-ok >/dev/null 2>&1 || return 1
printf '%s' "$response"
}
@@ -950,18 +901,11 @@ updates_fetch_github_releases_json() {
local owner="$1"
local repo="$2"
local per_page="${3:-30}"
local response message
local response
response="$(updates_http_get "https://api.github.com/repos/${owner}/${repo}/releases?per_page=${per_page}" 2>/dev/null || true)"
[ -n "$response" ] || return 1
printf '%s' "$response" | jq -e . >/dev/null 2>&1 || return 1
message="$(printf '%s' "$response" | jq -r 'if type == "object" then (.message // empty) else empty end' 2>/dev/null)"
case "$message" in
*"API rate limit"* | *"rate limit exceeded"* | "Not Found")
return 1
;;
esac
printf '%s' "$response" | json_utils_ucode github-response-ok >/dev/null 2>&1 || return 1
printf '%s' "$response"
}
@@ -995,27 +939,14 @@ updates_select_release_asset_name() {
local package_prefix="$2"
local asset_ext="$3"
printf '%s' "$release_json" | jq -r \
--arg prefix "$package_prefix" \
--arg ext "$asset_ext" \
'[
.assets[]
| select(
((.name | startswith($prefix + "_")) or (.name | startswith($prefix + "-")))
and (.name | endswith("." + $ext))
)
| .name
][0] // empty'
printf '%s' "$release_json" | json_utils_ucode release-asset-name "$package_prefix" "$asset_ext"
}
updates_select_release_asset_url() {
local release_json="$1"
local asset_name="$2"
printf '%s' "$release_json" | jq -r \
--arg name "$asset_name" \
'.assets[] | select(.name == $name) | .browser_download_url' |
sed -n '1p'
printf '%s' "$release_json" | json_utils_ucode release-asset-url "$asset_name" | sed -n '1p'
}
updates_resolve_podkop_plus_release() {
@@ -1039,13 +970,7 @@ updates_resolve_podkop_plus_release() {
updates_is_apk && asset_ext="apk"
response="$(updates_fetch_github_releases_json "$owner" "$repo" 50)" || return 1
release_json="$(printf '%s' "$response" | jq -c \
--arg tag "$latest_version" \
'.[] | select(
((.draft // false) | not)
and ((.prerelease // false) | not)
and (.tag_name == $tag)
)' | sed -n '1p')"
release_json="$(printf '%s' "$response" | json_utils_ucode release-by-tag "$latest_version" | sed -n '1p')"
[ -n "$release_json" ] || return 1
UPDATES_PODKOP_BACKEND_NAME="$(updates_select_release_asset_name "$release_json" "podkop-plus" "$asset_ext")"
@@ -1106,7 +1031,7 @@ updates_resolve_zapret_release() {
release_json="$(updates_fetch_github_release_json "remittor" "zapret-openwrt")" || return 1
for arch in $UPDATES_ARCH_CANDIDATES; do
candidate_name="$(printf '%s' "$release_json" | jq -r --arg arch "$arch" '[.assets[] | select(.name | endswith("_" + $arch + ".zip")) | .name][0] // empty')"
candidate_name="$(printf '%s' "$release_json" | json_utils_ucode release-asset-name-by-suffix "_${arch}.zip" 2>/dev/null)"
if [ -n "$candidate_name" ]; then
UPDATES_ZAPRET_ARCH="$arch"
UPDATES_ZAPRET_BUNDLE_NAME="$candidate_name"
@@ -1116,7 +1041,7 @@ updates_resolve_zapret_release() {
[ -n "$UPDATES_ZAPRET_BUNDLE_NAME" ] || return 1
url="$(printf '%s' "$release_json" | jq -r --arg name "$UPDATES_ZAPRET_BUNDLE_NAME" '[.assets[] | select(.name == $name) | .browser_download_url][0] // empty')"
url="$(printf '%s' "$release_json" | json_utils_ucode release-asset-url "$UPDATES_ZAPRET_BUNDLE_NAME" 2>/dev/null)"
[ -n "$url" ] || return 1
UPDATES_ZAPRET_BUNDLE_URL="$url"
UPDATES_ZAPRET_PACKAGE_VERSION="$(updates_extract_zapret_bundle_version "$UPDATES_ZAPRET_BUNDLE_NAME")"
@@ -1166,20 +1091,7 @@ updates_resolve_byedpi_release() {
response="$(updates_fetch_github_releases_json "DPITrickster" "ByeDPI-OpenWrt" 30)" || return 1
if [ -n "$release_series" ]; then
resolved="$(printf '%s' "$response" | jq -c --arg series "$release_series" '
.[]
| select(((.draft // false) | not) and ((.prerelease // false) | not))
| select(((.tag_name // "") | contains($series)) or ((.name // "") | contains($series)))
' | updates_select_byedpi_asset "$asset_ext")"
fi
if [ -z "$resolved" ]; then
resolved="$(printf '%s' "$response" | jq -c '
.[]
| select(((.draft // false) | not) and ((.prerelease // false) | not))
' | updates_select_byedpi_asset "$asset_ext")"
fi
resolved="$(printf '%s' "$response" | json_utils_ucode byedpi-select-asset "$release_series" "$asset_ext" "$UPDATES_ARCH_CANDIDATES" 2>/dev/null)"
[ -n "$resolved" ] || return 1
@@ -1194,36 +1106,6 @@ updates_resolve_byedpi_release() {
UPDATES_BYEDPI_PACKAGE_VERSION="$(updates_extract_arch_package_version "$UPDATES_BYEDPI_PACKAGE_NAME" "$UPDATES_BYEDPI_ARCH")"
}
updates_select_byedpi_asset() {
local asset_ext="$1"
local release_json candidate_name arch url resolved
while IFS= read -r release_json; do
[ -n "$release_json" ] || continue
[ -n "$resolved" ] && continue
for arch in $UPDATES_ARCH_CANDIDATES; do
candidate_name="$(printf '%s' "$release_json" | jq -r --arg arch "$arch" --arg ext "$asset_ext" '
[
(.assets // [])[]
| select(((.name | startswith("byedpi_")) or (.name | startswith("byedpi-"))) and (.name | endswith("." + $ext)))
| select((.name | contains("_" + $arch + "." + $ext)) or (.name | contains("-" + $arch + "." + $ext)))
| .name
][0] // empty
')"
if [ -n "$candidate_name" ]; then
url="$(printf '%s' "$release_json" | jq -r --arg name "$candidate_name" '[(.assets // [])[] | select(.name == $name) | .browser_download_url][0] // empty')"
[ -n "$url" ] || continue
resolved="$(printf '%s\t%s\t%s\n' "$arch" "$candidate_name" "$url")"
break
fi
done
done
[ -n "$resolved" ] && printf '%s\n' "$resolved"
}
updates_download_byedpi_package() {
UPDATES_BYEDPI_PACKAGE_FILE="$UPDATES_TMP_DIR/$UPDATES_BYEDPI_PACKAGE_NAME"
updates_download_with_retry "$UPDATES_BYEDPI_PACKAGE_URL" "$UPDATES_BYEDPI_PACKAGE_FILE" "$UPDATES_BYEDPI_PACKAGE_NAME" || return 1
@@ -1404,22 +1286,13 @@ updates_resolve_sing_box_extended_release() {
updates_resolve_sing_box_extended_arch_suffix || return 1
response="$(updates_fetch_github_releases_json "shtorm-7" "sing-box-extended" 30)" || return 1
tag="$(printf '%s' "$response" | jq -r '
[
.[]
| select(((.prerelease // false) | not) and ((.draft // false) | not))
| .tag_name // empty
| select(((ascii_downcase | contains("alpha")) or (ascii_downcase | contains("beta")) or (ascii_downcase | contains("rc"))) | not)
][0] // empty
')"
tag="$(printf '%s' "$response" | json_utils_ucode sing-box-extended-release-tag 2>/dev/null)"
[ -n "$tag" ] || return 1
UPDATES_SING_BOX_EXTENDED_RELEASE_TAG="$tag"
release_json="$(printf '%s' "$response" | jq -c --arg tag "$tag" '[.[] | select(.tag_name == $tag)][0] // empty')"
release_json="$(printf '%s' "$response" | json_utils_ucode release-by-tag "$tag" 2>/dev/null)"
asset_pattern="linux-${UPDATES_SING_BOX_EXTENDED_ARCH_SUFFIX}.tar.gz"
UPDATES_SING_BOX_EXTENDED_ASSET_URL="$(printf '%s' "$release_json" | jq -r --arg pattern "$asset_pattern" '
[(.assets // [])[] | select(.name | endswith($pattern)) | .browser_download_url][0] // empty
')"
UPDATES_SING_BOX_EXTENDED_ASSET_URL="$(printf '%s' "$release_json" | json_utils_ucode release-asset-url-by-suffix "$asset_pattern" 2>/dev/null)"
[ -n "$UPDATES_SING_BOX_EXTENDED_ASSET_URL" ] || return 1
UPDATES_SING_BOX_EXTENDED_ASSET_NAME="$(basename "$UPDATES_SING_BOX_EXTENDED_ASSET_URL")"
+30 -72
View File
@@ -820,12 +820,7 @@ zapret_rule_outbound_present() {
outbound_tag="$(get_outbound_tag_by_section "$section")"
jq -e \
--arg tag "$outbound_tag" \
--argjson routing_mark "$routing_mark" \
'.outbounds[]? |
select(.type == "direct" and .tag == $tag and (.routing_mark // empty) == $routing_mark)' \
"$ZAPRET_SINGBOX_CONFIG_PATH" >/dev/null 2>&1
provider_status_ucode has-direct-mark-outbound "$ZAPRET_SINGBOX_CONFIG_PATH" "$outbound_tag" "$routing_mark" >/dev/null 2>&1
}
zapret_rule_route_rule_present() {
@@ -836,12 +831,7 @@ zapret_rule_route_rule_present() {
outbound_tag="$(get_outbound_tag_by_section "$section")"
jq -e \
--arg inbound "$SB_TPROXY_INBOUND_TAG" \
--arg outbound "$outbound_tag" \
'.route.rules[]? |
select(.action == "route" and .inbound == $inbound and .outbound == $outbound)' \
"$ZAPRET_SINGBOX_CONFIG_PATH" >/dev/null 2>&1
provider_status_ucode has-route-rule "$ZAPRET_SINGBOX_CONFIG_PATH" "$SB_TPROXY_INBOUND_TAG" "$outbound_tag" >/dev/null 2>&1
}
_collect_zapret_runtime_status_handler() {
@@ -992,57 +982,33 @@ get_zapret_status_json() {
status_message="zapret provider status is normal"
fi
jq -cn \
--arg version "$version" \
--arg provider_path "$ZAPRET_PROVIDER_NFQWS_BIN" \
--arg status_message "$status_message" \
--argjson installed "$installed" \
--argjson package_installed "$package_installed" \
--argjson provider_available "$provider_available" \
--argjson files_available "$files_available" \
--argjson ipset_available "$ipset_available" \
--argjson configured "$configured" \
--argjson enabled_rule_count "${enabled_rule_count:-0}" \
--argjson expected_process_count "${expected_process_count:-0}" \
--argjson running_process_count "${running_process_count:-0}" \
--argjson supervisor_process_count "${supervisor_process_count:-0}" \
--argjson standalone_service_enabled "$standalone_service_enabled" \
--argjson standalone_service_running "$standalone_service_running" \
--argjson standalone_config_present "$standalone_config_present" \
--argjson standalone_conflict "$standalone_conflict" \
--argjson luci_app_installed "$luci_app_installed" \
--argjson queue_base "$ZAPRET_QUEUE_BASE" \
--argjson queue_range_end "$queue_range_end" \
--argjson queue_overlap "$queue_overlap" \
--argjson legacy_runtime_present "$legacy_runtime_present" \
--argjson ready "$ready" \
--argjson conflict "$conflict" \
'{
installed: $installed,
package_installed: $package_installed,
provider_available: $provider_available,
provider_path: $provider_path,
files_available: $files_available,
ipset_available: $ipset_available,
version: $version,
configured: $configured,
enabled_rule_count: $enabled_rule_count,
expected_process_count: $expected_process_count,
running_process_count: $running_process_count,
supervisor_process_count: $supervisor_process_count,
standalone_service_enabled: $standalone_service_enabled,
standalone_service_running: $standalone_service_running,
standalone_config_present: $standalone_config_present,
standalone_conflict: $standalone_conflict,
luci_app_installed: $luci_app_installed,
queue_base: $queue_base,
queue_range_end: $queue_range_end,
queue_overlap: $queue_overlap,
legacy_runtime_present: $legacy_runtime_present,
ready: $ready,
conflict: $conflict,
status_message: $status_message
}'
provider_status_ucode zapret-status \
"$installed" \
"$package_installed" \
"$provider_available" \
"$ZAPRET_PROVIDER_NFQWS_BIN" \
"$files_available" \
"$ipset_available" \
"$version" \
"$configured" \
"${enabled_rule_count:-0}" \
"${expected_process_count:-0}" \
"${running_process_count:-0}" \
"${supervisor_process_count:-0}" \
"$standalone_service_enabled" \
"$standalone_service_running" \
"$standalone_config_present" \
"$standalone_conflict" \
"$luci_app_installed" \
"$ZAPRET_QUEUE_BASE" \
"$queue_range_end" \
"$queue_overlap" \
"$legacy_runtime_present" \
"$ready" \
"$conflict" \
"$outbounds_configured" \
"$routes_configured" \
"$status_message"
}
check_zapret_runtime_json() {
@@ -1056,15 +1022,7 @@ check_zapret_runtime_json() {
package_installed=1
fi
jq -cn \
--argjson zapret_installed "$installed" \
--argjson zapret_package_installed "$package_installed" \
--arg zapret_provider_path "$ZAPRET_PROVIDER_NFQWS_BIN" \
'{
zapret_installed: $zapret_installed,
zapret_package_installed: $zapret_package_installed,
zapret_provider_path: $zapret_provider_path
}'
provider_status_ucode zapret-check "$installed" "$package_installed" "$ZAPRET_PROVIDER_NFQWS_BIN"
}
_create_zapret_nft_rule_handler() {
+2 -2
View File
@@ -41,8 +41,8 @@ APP_DESCRIPTION="Rule-based Podkop Plus LuCI app with hybrid sing-box + zapret o
I18N_DESCRIPTION="Translation for luci-app-podkop-plus - Русский (Russian)"
MAINTAINER="ushan0v <ushan0v@users.noreply.github.com>"
PROJECT_URL="https://github.com/ushan0v/podkop-plus"
BACKEND_DEPENDS_IPK="libc, sing-box, curl, jq, lua, luci-lib-jsonc, luci-lib-nixio, kmod-nft-tproxy, coreutils-base64, coreutils-sort, bind-dig, nftables, kmod-nft-nat, kmod-nft-offload"
BACKEND_DEPENDS_APK="bind-dig coreutils-base64 coreutils-sort curl jq lua luci-lib-jsonc luci-lib-nixio kmod-nft-nat kmod-nft-offload kmod-nft-tproxy libc nftables sing-box"
BACKEND_DEPENDS_IPK="libc, sing-box, curl, ucode, ucode-mod-fs, kmod-nft-tproxy, coreutils-base64, coreutils-sort, bind-dig, nftables, kmod-nft-nat, kmod-nft-offload"
BACKEND_DEPENDS_APK="bind-dig coreutils-base64 coreutils-sort curl ucode ucode-mod-fs kmod-nft-nat kmod-nft-offload kmod-nft-tproxy libc nftables sing-box"
APP_DEPENDS_IPK="libc, luci-base, podkop-plus"
APP_DEPENDS_APK="libc luci-base podkop-plus"