Resolves merge conflicts: - install.sh, build.sh, .github/workflows/build.yml: Reverted to Dushnilin's versions to avoid installing SourceForge mirrors. - fe-app-tachyon: Resolved type, formatting and copyOutbound conflicts, keeping both Dushnilin's custom features and ushan0v's frontend performance optimizations. - backend: Kept Dushnilin's ucode refactorings, Telegram bot integration, and release repository URL (Dushnilin/tachyon) while merging updates.uc cache format v8 & persistent cache format v7. - tests: Merged tests from both branches, preserving ucode -L wrappers for local test execution.
544 lines
16 KiB
Ucode
544 lines
16 KiB
Ucode
#!/usr/bin/env ucode
|
|
|
|
let fs = require("fs");
|
|
|
|
const SB_DNS_SERVER_TAG = getenv("SB_DNS_SERVER_TAG") || "dns-server";
|
|
const SB_FAKEIP_DNS_SERVER_TAG = getenv("SB_FAKEIP_DNS_SERVER_TAG") || "fakeip-server";
|
|
const SB_BOOTSTRAP_SERVER_TAG = getenv("SB_BOOTSTRAP_SERVER_TAG") || "bootstrap-dns-server";
|
|
const SB_FAKEIP_DNS_RULE_TAG = getenv("SB_FAKEIP_DNS_RULE_TAG") || "fakeip-dns-rule-tag";
|
|
const SB_FAKEIP_RULESET_DNS_RULE_TAG = getenv("SB_FAKEIP_RULESET_DNS_RULE_TAG") || "fakeip-ruleset-dns-rule-tag";
|
|
const SB_SERVICE_FAKEIP_DNS_RULE_TAG = getenv("SB_SERVICE_FAKEIP_DNS_RULE_TAG") || "service-fakeip-dns-rule-tag";
|
|
const SB_TPROXY_INBOUND_TAG = getenv("SB_TPROXY_INBOUND_TAG") || "tproxy-in";
|
|
const SB_TPROXY_INBOUND6_TAG = getenv("SB_TPROXY_INBOUND6_TAG") || "tproxy6-in";
|
|
const SB_DNS_INBOUND_TAG = getenv("SB_DNS_INBOUND_TAG") || "dns-in";
|
|
const SB_SERVICE_MIXED_INBOUND_TAG = getenv("SB_SERVICE_MIXED_INBOUND_TAG") || "service-mixed-in";
|
|
const SB_DIRECT_OUTBOUND_TAG = getenv("SB_DIRECT_OUTBOUND_TAG") || "direct-out";
|
|
const SB_BYPASS_OUTBOUND_TAG = getenv("SB_BYPASS_OUTBOUND_TAG") || "bypass-out";
|
|
|
|
let common = require("core.common");
|
|
let core_ip = require("core.ip");
|
|
let as_string = common.as_string;
|
|
|
|
function ascii_lower(value) {
|
|
return lc(as_string(value));
|
|
}
|
|
|
|
|
|
|
|
function file_has_cr(path) {
|
|
let data = fs.readfile(as_string(path));
|
|
if (data == null)
|
|
return null;
|
|
|
|
return index(data, "\r") >= 0;
|
|
}
|
|
|
|
function file_has_cr_exit(path) {
|
|
let result = file_has_cr(path);
|
|
exit(result == null ? 2 : (result ? 0 : 1));
|
|
}
|
|
|
|
function write_text_file(path, text) {
|
|
let result = fs.writefile(path, as_string(text));
|
|
if (result == null)
|
|
return false;
|
|
if (type(result) == "boolean" && !result)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
function file_remove_cr(path) {
|
|
path = as_string(path);
|
|
let data = fs.readfile(path);
|
|
if (data == null)
|
|
return true;
|
|
|
|
if (index(data, "\r") < 0)
|
|
return true;
|
|
|
|
let stamp = clock();
|
|
let tmp_path = sprintf("%s.%d.%d.tmp", path, stamp[0], stamp[1]);
|
|
if (!write_text_file(tmp_path, replace(data, /\r/g, "")))
|
|
return false;
|
|
|
|
if (!fs.rename(tmp_path, path)) {
|
|
fs.unlink(tmp_path);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
function valid_ipv4(value) {
|
|
return core_ip.valid_ipv4(value, true, true);
|
|
}
|
|
|
|
function valid_ipv4_cidr(value) {
|
|
return core_ip.valid_ipv4_cidr(value, true);
|
|
}
|
|
|
|
function valid_domain(value) {
|
|
return match(ascii_lower(value), /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/) != null;
|
|
}
|
|
|
|
function valid_domain_suffix(value) {
|
|
value = as_string(value);
|
|
if (substr(value, 0, 1) == ".")
|
|
value = substr(value, 1);
|
|
|
|
return valid_domain(value);
|
|
}
|
|
|
|
function digit_char(value) {
|
|
return match(as_string(value), /^[0-9]$/) != null;
|
|
}
|
|
|
|
function strip_leading_zeroes(value) {
|
|
value = as_string(value);
|
|
let i = 0;
|
|
while (i < length(value) - 1 && substr(value, i, 1) == "0")
|
|
i++;
|
|
return substr(value, i);
|
|
}
|
|
|
|
function version_compare(lhs, rhs) {
|
|
lhs = as_string(lhs);
|
|
rhs = as_string(rhs);
|
|
|
|
let li = 0, ri = 0;
|
|
while (li < length(lhs) || ri < length(rhs)) {
|
|
if (li >= length(lhs))
|
|
return substr(rhs, ri, 1) == "~" ? 1 : -1;
|
|
if (ri >= length(rhs))
|
|
return substr(lhs, li, 1) == "~" ? -1 : 1;
|
|
|
|
let lc = substr(lhs, li, 1);
|
|
let rc = substr(rhs, ri, 1);
|
|
if (lc == rc) {
|
|
li++;
|
|
ri++;
|
|
continue;
|
|
}
|
|
|
|
if (lc == "~" || rc == "~")
|
|
return lc == "~" ? -1 : 1;
|
|
|
|
if (digit_char(lc) && digit_char(rc)) {
|
|
let ls = li, rs = ri;
|
|
while (li < length(lhs) && digit_char(substr(lhs, li, 1)))
|
|
li++;
|
|
while (ri < length(rhs) && digit_char(substr(rhs, ri, 1)))
|
|
ri++;
|
|
|
|
let lnum = strip_leading_zeroes(substr(lhs, ls, li - ls));
|
|
let rnum = strip_leading_zeroes(substr(rhs, rs, ri - rs));
|
|
if (length(lnum) != length(rnum))
|
|
return length(lnum) < length(rnum) ? -1 : 1;
|
|
if (lnum != rnum)
|
|
return lnum < rnum ? -1 : 1;
|
|
continue;
|
|
}
|
|
|
|
return lc < rc ? -1 : 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
function version_at_least(current, required) {
|
|
return version_compare(current, required) >= 0;
|
|
}
|
|
|
|
function first_index_any(value, needles, start) {
|
|
value = as_string(value);
|
|
start = int(start || 0);
|
|
|
|
for (let i = start; i < length(value); i++) {
|
|
let c = substr(value, i, 1);
|
|
for (let needle in needles)
|
|
if (c == needle)
|
|
return i;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
function strip_anchored_scheme(value) {
|
|
value = as_string(value);
|
|
let marker = index(value, "://");
|
|
if (marker < 0)
|
|
return value;
|
|
|
|
let prefix = substr(value, 0, marker);
|
|
if (index(prefix, "/") >= 0 || index(prefix, "?") >= 0)
|
|
return value;
|
|
|
|
return substr(value, marker + 3);
|
|
}
|
|
|
|
function strip_first_scheme_marker(value) {
|
|
value = as_string(value);
|
|
let marker = index(value, "://");
|
|
return marker >= 0 ? substr(value, marker + 3) : value;
|
|
}
|
|
|
|
function url_get_scheme(value) {
|
|
value = as_string(value);
|
|
let marker = index(value, "://");
|
|
print(marker >= 0 ? substr(value, 0, marker) : value, "\n");
|
|
}
|
|
|
|
function url_get_userinfo(value) {
|
|
value = strip_anchored_scheme(value);
|
|
let at = index(value, "@");
|
|
if (at >= 0)
|
|
print(substr(value, 0, at), "\n");
|
|
}
|
|
|
|
function url_authority(value) {
|
|
value = strip_first_scheme_marker(value);
|
|
let at = index(value, "@");
|
|
if (at >= 0)
|
|
value = substr(value, at + 1);
|
|
|
|
let end = first_index_any(value, ["/", "?", "#"], 0);
|
|
return end >= 0 ? substr(value, 0, end) : value;
|
|
}
|
|
|
|
function url_get_host(value) {
|
|
let authority = url_authority(value);
|
|
let colon = index(authority, ":");
|
|
print(colon >= 0 ? substr(authority, 0, colon) : authority, "\n");
|
|
}
|
|
|
|
function url_get_port(value) {
|
|
let authority = url_authority(value);
|
|
let colon = index(authority, ":");
|
|
print(colon >= 0 ? substr(authority, colon + 1) : "", "\n");
|
|
}
|
|
|
|
function url_get_path(value) {
|
|
value = strip_anchored_scheme(value);
|
|
let slash = index(value, "/");
|
|
value = slash >= 0 ? substr(value, slash) : "";
|
|
|
|
let query = index(value, "?");
|
|
if (query >= 0)
|
|
value = substr(value, 0, query);
|
|
|
|
print(value, "\n");
|
|
}
|
|
|
|
function url_get_query_param(value, param) {
|
|
value = as_string(value);
|
|
param = as_string(param);
|
|
let result = null;
|
|
|
|
for (let i = 0; i < length(value); i++) {
|
|
let separator = substr(value, i, 1);
|
|
if (separator != "?" && separator != "&")
|
|
continue;
|
|
|
|
let prefix_start = i + 1;
|
|
if (substr(value, prefix_start, length(param) + 1) != param + "=")
|
|
continue;
|
|
|
|
let value_start = prefix_start + length(param) + 1;
|
|
let value_end = first_index_any(value, ["&", "?", "#"], value_start);
|
|
result = value_end >= 0 ? substr(value, value_start, value_end - value_start) : substr(value, value_start);
|
|
}
|
|
|
|
print(result != null ? result : "", "\n");
|
|
}
|
|
|
|
function url_file_extension(value) {
|
|
let basename = as_string(value);
|
|
let slash = rindex(basename, "/");
|
|
if (slash >= 0)
|
|
basename = substr(basename, slash + 1);
|
|
|
|
let query = index(basename, "?");
|
|
if (query >= 0)
|
|
basename = substr(basename, 0, query);
|
|
|
|
let fragment = index(basename, "#");
|
|
if (fragment >= 0)
|
|
basename = substr(basename, 0, fragment);
|
|
|
|
let dot = rindex(basename, ".");
|
|
if (dot >= 0)
|
|
print(substr(basename, dot + 1), "\n");
|
|
else
|
|
print("\n");
|
|
}
|
|
|
|
function url_strip_fragment(value) {
|
|
value = as_string(value);
|
|
let fragment = index(value, "#");
|
|
print(fragment >= 0 ? substr(value, 0, fragment) : value, "\n");
|
|
}
|
|
|
|
function normalize_strategy_whitespace(value) {
|
|
value = replace(as_string(value), /[\t\r\n]/g, " ");
|
|
value = replace(value, / +/g, " ");
|
|
value = replace(value, /^ /, "");
|
|
value = replace(value, / $/, "");
|
|
print(value);
|
|
}
|
|
|
|
function strip_list_comment(line) {
|
|
line = replace(as_string(line), /[[:space:]]*\/\/.*$/, "");
|
|
return replace(line, /[[:space:]]*#.*$/, "");
|
|
}
|
|
|
|
function print_lines(values) {
|
|
for (let value in values)
|
|
print(as_string(value), "\n");
|
|
}
|
|
|
|
function text_list_values(value, separator_mode) {
|
|
let result = [];
|
|
separator_mode = as_string(separator_mode);
|
|
|
|
for (let line in split(as_string(value), "\n")) {
|
|
line = strip_list_comment(line);
|
|
line = separator_mode == "comma-space"
|
|
? replace(line, /[ ,]/g, "\n")
|
|
: replace(line, /,/g, "\n");
|
|
|
|
for (let item in split(line, "\n")) {
|
|
item = trim(replace(item, /\r/g, ""));
|
|
if (item != "")
|
|
push(result, item);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function text_list_to_lines(value, separator_mode) {
|
|
print_lines(text_list_values(value, separator_mode));
|
|
}
|
|
|
|
function stdin_lines_to_json_array() {
|
|
let input = common.read_stdin();
|
|
if (input == "") {
|
|
print("[]\n");
|
|
return;
|
|
}
|
|
|
|
if (substr(input, length(input) - 1) == "\n")
|
|
input = substr(input, 0, length(input) - 1);
|
|
|
|
common.write_compact_string_array(split(input, "\n"));
|
|
}
|
|
|
|
function network_status_ipv4_address() {
|
|
let value = common.read_stdin_json();
|
|
if (type(value) != "object")
|
|
return;
|
|
|
|
let addresses = value["ipv4-address"];
|
|
if (type(addresses) != "array" || length(addresses) == 0 || type(addresses[0]) != "object")
|
|
return;
|
|
|
|
let address = as_string(addresses[0].address || "");
|
|
if (address != "")
|
|
print(address, "\n");
|
|
}
|
|
|
|
function stdin_first_line_last_field() {
|
|
let input = common.read_stdin();
|
|
if (input == "")
|
|
return;
|
|
|
|
let newline = index(input, "\n");
|
|
let line = newline >= 0 ? substr(input, 0, newline) : input;
|
|
let trimmed = trim(line);
|
|
if (trimmed == "") {
|
|
print(line, "\n");
|
|
return;
|
|
}
|
|
|
|
let fields = split(trimmed, /[ \t\r\n]+/);
|
|
if (length(fields) > 0 && fields[0] != "")
|
|
print(fields[length(fields) - 1], "\n");
|
|
else
|
|
print("\n");
|
|
}
|
|
|
|
function stdin_trim_string() {
|
|
print(trim(common.read_stdin()), "\n");
|
|
}
|
|
|
|
function whitespace_list_contains(list, needle) {
|
|
needle = as_string(needle);
|
|
for (let item in split(trim(as_string(list)), /[ \t\r\n]+/))
|
|
if (as_string(item) == needle)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
function download_via_proxy_option_for_purpose(purpose) {
|
|
purpose = as_string(purpose || "lists");
|
|
|
|
if (purpose == "lists")
|
|
print("download_lists_via_proxy\n");
|
|
else if (purpose == "components")
|
|
print("download_components_via_proxy\n");
|
|
else
|
|
exit(1);
|
|
}
|
|
|
|
function md5sum_hex_prefix(prefix_length) {
|
|
let input = common.read_stdin();
|
|
let newline = index(input, "\n");
|
|
let line = newline >= 0 ? substr(input, 0, newline) : input;
|
|
let fields = split(trim(as_string(line)), /[ \t\r\n]+/);
|
|
let hash = length(fields) > 0 ? as_string(fields[0]) : "";
|
|
|
|
prefix_length = int(prefix_length || 0);
|
|
if (prefix_length > 0)
|
|
print(substr(hash, 0, prefix_length), "\n");
|
|
}
|
|
|
|
function md5sum_hwid() {
|
|
let input = common.read_stdin();
|
|
let newline = index(input, "\n");
|
|
let line = newline >= 0 ? substr(input, 0, newline) : input;
|
|
let fields = split(trim(as_string(line)), /[ \t\r\n]+/);
|
|
let hash = length(fields) > 0 ? substr(as_string(fields[0]), 0, 16) : "";
|
|
|
|
print(
|
|
substr(hash, 0, 4), "-",
|
|
substr(hash, 4, 4), "-",
|
|
substr(hash, 8, 4), "-",
|
|
substr(hash, 12, 4), "\n"
|
|
);
|
|
}
|
|
|
|
function tag_is_reserved(tag, reserved) {
|
|
tag = as_string(tag);
|
|
for (let value in reserved)
|
|
if (tag == as_string(value))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
function allocate_runtime_tag_value(base, postfix, reserved) {
|
|
base = as_string(base);
|
|
postfix = as_string(postfix);
|
|
reserved = type(reserved) == "array" ? reserved : [];
|
|
let candidate = base + "-" + postfix;
|
|
let suffix = 1;
|
|
|
|
while (tag_is_reserved(candidate, reserved)) {
|
|
candidate = base + "-" + postfix + "-" + suffix;
|
|
suffix++;
|
|
}
|
|
|
|
return candidate;
|
|
}
|
|
|
|
function default_reserved_runtime_tags() {
|
|
return [
|
|
SB_DNS_SERVER_TAG,
|
|
SB_FAKEIP_DNS_SERVER_TAG,
|
|
SB_BOOTSTRAP_SERVER_TAG,
|
|
SB_FAKEIP_DNS_RULE_TAG,
|
|
SB_FAKEIP_RULESET_DNS_RULE_TAG,
|
|
SB_SERVICE_FAKEIP_DNS_RULE_TAG,
|
|
SB_TPROXY_INBOUND_TAG,
|
|
SB_TPROXY_INBOUND6_TAG,
|
|
SB_DNS_INBOUND_TAG,
|
|
SB_SERVICE_MIXED_INBOUND_TAG,
|
|
SB_DIRECT_OUTBOUND_TAG,
|
|
SB_BYPASS_OUTBOUND_TAG
|
|
];
|
|
}
|
|
|
|
function print_runtime_tag(base, postfix, reserved) {
|
|
print(allocate_runtime_tag_value(base, postfix, reserved), "\n");
|
|
}
|
|
|
|
function allocate_runtime_tag(base, postfix) {
|
|
print_runtime_tag(base, postfix, slice(ARGV, 3));
|
|
}
|
|
|
|
function sing_box_version_is_extended(value) {
|
|
return index(as_string(value), "extended") >= 0;
|
|
}
|
|
|
|
let mode = ARGV[0] || "";
|
|
|
|
if (mode == "stdin-lines-to-json-array")
|
|
stdin_lines_to_json_array();
|
|
else if (mode == "file-has-cr")
|
|
file_has_cr_exit(ARGV[1]);
|
|
else if (mode == "file-remove-cr")
|
|
exit(file_remove_cr(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "valid-ipv4")
|
|
exit(valid_ipv4(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "valid-ipv4-cidr")
|
|
exit(valid_ipv4_cidr(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "valid-domain")
|
|
exit(valid_domain(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "valid-domain-suffix")
|
|
exit(valid_domain_suffix(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "version-at-least")
|
|
exit(version_at_least(ARGV[1], ARGV[2]) ? 0 : 1);
|
|
else if (mode == "network-status-ipv4-address")
|
|
network_status_ipv4_address();
|
|
else if (mode == "stdin-first-line-last-field")
|
|
stdin_first_line_last_field();
|
|
else if (mode == "stdin-trim-string")
|
|
stdin_trim_string();
|
|
else if (mode == "whitespace-list-contains")
|
|
exit(whitespace_list_contains(ARGV[1], ARGV[2]) ? 0 : 1);
|
|
else if (mode == "download-via-proxy-option-for-purpose")
|
|
download_via_proxy_option_for_purpose(ARGV[1]);
|
|
else if (mode == "md5sum-hex-prefix")
|
|
md5sum_hex_prefix(ARGV[1]);
|
|
else if (mode == "md5sum-hwid")
|
|
md5sum_hwid();
|
|
else if (mode == "allocate-runtime-tag")
|
|
allocate_runtime_tag(ARGV[1], ARGV[2]);
|
|
else if (mode == "inbound-tag")
|
|
print_runtime_tag(ARGV[1], "in", default_reserved_runtime_tags());
|
|
else if (mode == "server-inbound-tag")
|
|
print_runtime_tag("server-" + as_string(ARGV[1]), "in", default_reserved_runtime_tags());
|
|
else if (mode == "tailscale-dns-tag")
|
|
print_runtime_tag("server-" + as_string(ARGV[1]), "tailscale-dns", default_reserved_runtime_tags());
|
|
else if (mode == "outbound-tag")
|
|
print_runtime_tag(ARGV[1], "out", default_reserved_runtime_tags());
|
|
else if (mode == "domain-resolver-tag")
|
|
print_runtime_tag(ARGV[1], "domain-resolver", default_reserved_runtime_tags());
|
|
else if (mode == "sing-box-version-is-extended")
|
|
exit(sing_box_version_is_extended(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "url-get-scheme")
|
|
url_get_scheme(ARGV[1]);
|
|
else if (mode == "url-get-userinfo")
|
|
url_get_userinfo(ARGV[1]);
|
|
else if (mode == "url-get-host")
|
|
url_get_host(ARGV[1]);
|
|
else if (mode == "url-get-port")
|
|
url_get_port(ARGV[1]);
|
|
else if (mode == "url-get-path")
|
|
url_get_path(ARGV[1]);
|
|
else if (mode == "url-get-query-param")
|
|
url_get_query_param(ARGV[1], ARGV[2]);
|
|
else if (mode == "url-file-extension")
|
|
url_file_extension(ARGV[1]);
|
|
else if (mode == "url-strip-fragment")
|
|
url_strip_fragment(ARGV[1]);
|
|
else if (mode == "normalize-strategy-whitespace")
|
|
normalize_strategy_whitespace(ARGV[1]);
|
|
else if (mode == "text-list-to-lines")
|
|
text_list_to_lines(ARGV[1], ARGV[2]);
|
|
else {
|
|
warn("Usage: core/helpers.uc <operation> ...\n");
|
|
exit(1);
|
|
}
|