Replace runtime Lua and jq with ucode
This commit is contained in:
+157
-16
@@ -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
@@ -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
|
||||
|
||||
+308
-416
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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")"
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user