Files
NeoTachyon/tachyon/files/usr/lib/subscription/parser.uc
T

3048 lines
96 KiB
Ucode

#!/usr/bin/env ucode
let fs = require("fs");
function as_string(value) {
return value == null ? "" : "" + value;
}
function first_non_ws_char(value) {
let m = match(as_string(value), /^[ \t\r\n]*([^ \t\r\n])/);
return m ? m[1] : "";
}
function starts_with(value, prefix) {
value = as_string(value);
prefix = as_string(prefix);
return substr(value, 0, length(prefix)) == prefix;
}
function is_supported_share_link(line) {
return starts_with(line, "ss://") ||
starts_with(line, "vmess://") ||
starts_with(line, "vless://") ||
starts_with(line, "trojan://") ||
starts_with(line, "hysteria2://") ||
starts_with(line, "hy2://") ||
starts_with(line, "tuic://") ||
starts_with(line, "socks://") ||
starts_with(line, "socks4://") ||
starts_with(line, "socks4a://") ||
starts_with(line, "socks5://");
}
function split_csv(value) {
let result = [];
for (let item in split(as_string(value), ",")) {
if (item != "")
push(result, item);
}
return result;
}
function tls_alpn_for_transport(alpn, transport) {
transport = lc(as_string(transport));
if (transport == "xhttp" && length(alpn) == 0)
return ["h2", "http/1.1"];
if ((transport == "ws" || transport == "httpupgrade") && length(alpn) > 0)
return ["http/1.1"];
return alpn;
}
function is_true(value) {
if (value == null || value == "")
return false;
let normalized = lc(as_string(value));
return normalized == "1" || normalized == "true" || normalized == "yes" || normalized == "on";
}
function is_integer_string(value) {
return type(value) == "string" && match(value, /^[0-9]+$/) != null;
}
function xhttp_value_present(value) {
return value != null && as_string(value) != "";
}
function xhttp_object_arg(value) {
if (type(value) == "object")
return value;
try {
let parsed = json(as_string(value));
return type(parsed) == "object" ? parsed : {};
}
catch (e) {
return {};
}
}
function xhttp_copy_known_settings(target, source) {
if (type(source) != "object")
return;
for (let key in [
"xPaddingBytes",
"x_padding_bytes",
"noGRPCHeader",
"no_grpc_header",
"scMaxEachPostBytes",
"sc_max_each_post_bytes",
"scMinPostsIntervalMs",
"sc_min_posts_interval_ms",
"scStreamUpServerSecs",
"sc_stream_up_server_secs",
"xmux"
]) {
if (xhttp_value_present(source[key]))
target[key] = source[key];
}
}
function xhttp_merge_extra_settings(target, value) {
let extra = xhttp_object_arg(value);
if (type(extra) != "object")
return;
xhttp_copy_known_settings(target, extra);
let xhttp_settings = xhttp_object_arg(extra.xhttpSettings);
xhttp_copy_known_settings(target, xhttp_settings);
xhttp_copy_known_settings(target, xhttp_object_arg(xhttp_settings.extra));
let download_settings = xhttp_object_arg(extra.downloadSettings);
let download_xhttp_settings = xhttp_object_arg(download_settings.xhttpSettings);
xhttp_copy_known_settings(target, download_xhttp_settings);
xhttp_copy_known_settings(target, xhttp_object_arg(download_xhttp_settings.extra));
}
function xhttp_extra_settings(query) {
let result = {};
query = type(query) == "object" ? query : {};
xhttp_merge_extra_settings(result, query.extra);
return result;
}
function xhttp_setting_value(query, extra_settings, camel_key, snake_key) {
query = type(query) == "object" ? query : {};
extra_settings = type(extra_settings) == "object" ? extra_settings : {};
for (let value in [query[camel_key], query[snake_key], extra_settings[camel_key], extra_settings[snake_key]]) {
if (xhttp_value_present(value))
return value;
}
return null;
}
function xhttp_non_negative_integer_value(value) {
if (type(value) == "int" || type(value) == "double") {
let number = int(value);
return number == value && number >= 0 ? number : null;
}
value = trim(as_string(value));
if (!is_integer_string(value))
return null;
return int(value, 10);
}
function xhttp_positive_integer_value(value) {
let number = xhttp_non_negative_integer_value(value);
return number != null && number > 0 ? number : null;
}
function xhttp_range_value(value, positive) {
if (!xhttp_value_present(value))
return null;
if (type(value) == "object") {
let from = positive ? xhttp_positive_integer_value(value.from) : xhttp_non_negative_integer_value(value.from);
let to = positive ? xhttp_positive_integer_value(value.to) : xhttp_non_negative_integer_value(value.to);
return from != null && to != null && from <= to ? (from == to ? as_string(from) : from + "-" + to) : null;
}
let number = positive ? xhttp_positive_integer_value(value) : xhttp_non_negative_integer_value(value);
if (number != null)
return as_string(number);
value = trim(as_string(value));
let dash = index(value, "-");
if (dash < 0 || index(substr(value, dash + 1), "-") >= 0)
return null;
let from = positive ? xhttp_positive_integer_value(substr(value, 0, dash)) : xhttp_non_negative_integer_value(substr(value, 0, dash));
let to = positive ? xhttp_positive_integer_value(substr(value, dash + 1)) : xhttp_non_negative_integer_value(substr(value, dash + 1));
return from != null && to != null && from <= to ? from + "-" + to : null;
}
function xhttp_optional_range(object, key, value) {
let normalized = xhttp_range_value(value, false);
if (normalized != null)
object[key] = normalized;
}
function xhttp_optional_positive_range(object, key, value) {
let normalized = xhttp_range_value(value, true);
if (normalized != null)
object[key] = normalized;
}
function xhttp_positive_range_or_default(value, default_value) {
let normalized = xhttp_range_value(value, true);
return normalized != null ? normalized : default_value;
}
function xhttp_present_positive_range_or_default(value, default_value) {
if (!xhttp_value_present(value))
return null;
return xhttp_positive_range_or_default(value, default_value);
}
function xhttp_optional_bool(object, key, value) {
if (xhttp_value_present(value))
object[key] = is_true(value);
}
function xhttp_object_setting_value(source, camel_key, snake_key) {
source = type(source) == "object" ? source : {};
for (let value in [source[camel_key], source[snake_key]]) {
if (xhttp_value_present(value))
return value;
}
return null;
}
function xhttp_optional_xmux_range(object, key, value) {
let normalized = xhttp_range_value(value, false);
if (normalized != null)
object[key] = normalized;
}
function xhttp_optional_xmux_integer(object, key, value) {
let normalized = xhttp_non_negative_integer_value(value);
if (normalized != null)
object[key] = normalized;
}
function xhttp_normalize_xmux(value) {
let source = xhttp_object_arg(value);
if (type(source) != "object")
return null;
let result = {};
xhttp_optional_xmux_range(result, "max_concurrency", xhttp_object_setting_value(source, "maxConcurrency", "max_concurrency"));
xhttp_optional_xmux_range(result, "max_connections", xhttp_object_setting_value(source, "maxConnections", "max_connections"));
xhttp_optional_xmux_range(result, "c_max_reuse_times", xhttp_object_setting_value(source, "cMaxReuseTimes", "c_max_reuse_times"));
xhttp_optional_xmux_range(result, "h_max_request_times", xhttp_object_setting_value(source, "hMaxRequestTimes", "h_max_request_times"));
xhttp_optional_xmux_range(result, "h_max_reusable_secs", xhttp_object_setting_value(source, "hMaxReusableSecs", "h_max_reusable_secs"));
xhttp_optional_xmux_integer(result, "h_keep_alive_period", xhttp_object_setting_value(source, "hKeepAlivePeriod", "h_keep_alive_period"));
return length(keys(result)) > 0 ? result : null;
}
function json_decode_text(text) {
try {
return json(as_string(text));
}
catch (e) {
return null;
}
}
function read_file(path) {
let stat = fs.stat(path);
if (stat != null && stat.size != null && stat.size > 15728640) {
warn(sprintf("File %s exceeds maximum size limit (15MB)\n", path));
return null;
}
return fs.readfile(path);
}
function read_json_file(path) {
let data = read_file(path);
return data == null ? null : json_decode_text(data);
}
function reality_public_key_valid(value) {
return match(as_string(value), /^[A-Za-z0-9_-]{43}$/) != null;
}
function validate_subscription(path) {
let value = read_json_file(path);
if (type(value) != "object" || type(value.outbounds) != "array" || length(value.outbounds) == 0)
return false;
for (let outbound in value.outbounds) {
let tls = type(outbound) == "object" && type(outbound.tls) == "object" ? outbound.tls : {};
let reality = tls.reality;
if (type(reality) == "object" && reality.enabled !== false && !reality_public_key_valid(reality.public_key))
return false;
}
return true;
}
function write_json_file(path, value) {
return fs.writefile(path, sprintf("%J", value) + "\n");
}
function write_json(value) {
print(sprintf("%J", value), "\n");
}
function canonical_runtime_value(value) {
if (type(value) == "array") {
let result = [];
for (let item in value)
push(result, canonical_runtime_value(item));
return result;
}
if (type(value) == "object") {
let result = {};
let names = keys(value);
sort(names, function(a, b) {
return a < b ? -1 : (a > b ? 1 : 0);
});
for (let name in names) {
if (name == "share_link")
continue;
result[name] = canonical_runtime_value(value[name]);
}
return result;
}
return value;
}
function runtime_outbounds_signature(path) {
let subscription = read_json_file(path);
if (type(subscription) != "object" || type(subscription.outbounds) != "array")
return null;
return canonical_runtime_value(subscription.outbounds);
}
function runtime_outbounds_equal(left_path, right_path) {
let left = runtime_outbounds_signature(left_path);
let right = runtime_outbounds_signature(right_path);
if (left == null || right == null)
return false;
return sprintf("%J", left) == sprintf("%J", right);
}
function array_or_empty(value) {
return type(value) == "array" ? value : [];
}
function object_or_empty(value) {
return type(value) == "object" ? value : {};
}
function base64_decode(value) {
value = as_string(value);
if (index(value, "\n") >= 0 || index(value, "\r") >= 0 || index(value, "\t") >= 0 || index(value, " ") >= 0)
value = replace(value, /\s+/g, "");
if (index(value, "-") >= 0)
value = replace(value, /-/g, "+");
if (index(value, "_") >= 0)
value = replace(value, /_/g, "/");
if (value == "")
return null;
let remainder = length(value) % 4;
if (remainder == 1)
return null;
if (remainder > 1) {
for (let i = 0; i < 4 - remainder; i++)
value += "=";
}
return b64dec(value);
}
function urldecode(value) {
value = as_string(value);
if (index(value, "%") < 0 && index(value, "+") < 0)
return value;
if (index(value, "+") >= 0)
value = replace(value, /\+/g, " ");
if (index(value, "%") < 0)
return value;
return replace(value, /%([0-9A-Fa-f][0-9A-Fa-f])/g, function(all, hex_value) {
return chr(hex(hex_value));
});
}
let fragment_prefix_decode_cache = {};
let fragment_decode_cache = {};
let query_parse_cache = {};
function urldecode_fragment(value) {
value = as_string(value);
if (index(value, "%") < 0 && index(value, "+") < 0)
return value;
if (value in fragment_decode_cache)
return fragment_decode_cache[value];
let marker = rindex(value, "%23");
if (marker >= 0) {
let suffix = substr(value, marker + 3);
if (is_integer_string(suffix)) {
let prefix = substr(value, 0, marker + 3);
if (!(prefix in fragment_prefix_decode_cache))
fragment_prefix_decode_cache[prefix] = urldecode(prefix);
let decoded = fragment_prefix_decode_cache[prefix] + suffix;
fragment_decode_cache[value] = decoded;
return decoded;
}
}
let decoded = urldecode(value);
fragment_decode_cache[value] = decoded;
return decoded;
}
function subscription_url_fragment(url) {
url = as_string(url);
let marker = index(url, "#");
if (marker < 0) {
print("\n");
return true;
}
let fragment = substr(url, marker + 1);
if (index(fragment, "%") >= 0 || index(fragment, "+") >= 0)
print(urldecode_fragment(fragment));
else
print(fragment, "\n");
return true;
}
function parse_query(query) {
let params = {};
query = as_string(query);
if (query == "")
return params;
if (query in query_parse_cache)
return query_parse_cache[query];
for (let pair in split(query, "&")) {
if (pair == "")
continue;
let equals = index(pair, "=");
let key = equals >= 0 ? substr(pair, 0, equals) : pair;
let value = equals >= 0 ? substr(pair, equals + 1) : "";
key = urldecode(key);
if (key != "")
params[key] = urldecode(value);
}
query_parse_cache[query] = params;
return params;
}
function parse_port_value(value) {
value = as_string(value);
return is_integer_string(value) ? int(value, 10) : value;
}
function parse_host_port(value) {
value = as_string(value);
if (length(value) > 0 && substr(value, length(value) - 1) == "/")
value = substr(value, 0, length(value) - 1);
if (starts_with(value, "[")) {
let m = match(value, /^\[([^\]]+)\]:(.+)$/);
return m ? [m[1], parse_port_value(m[2])] : ["", null];
}
let colon = rindex(value, ":");
if (colon < 0)
return ["", null];
let host = substr(value, 0, colon);
let port = substr(value, colon + 1);
return [host, parse_port_value(port)];
}
function parse_url(url) {
let scheme_pos = index(url, "://");
if (scheme_pos <= 0)
return null;
let scheme = lc(substr(url, 0, scheme_pos));
let rest = substr(url, scheme_pos + 3);
let fragment = "";
let hash_pos = index(rest, "#");
if (hash_pos >= 0) {
fragment = urldecode_fragment(substr(rest, hash_pos + 1));
rest = substr(rest, 0, hash_pos);
}
let query = "";
let question_pos = index(rest, "?");
if (question_pos >= 0) {
query = substr(rest, question_pos + 1);
rest = substr(rest, 0, question_pos);
}
let slash_pos = index(rest, "/");
let authority = slash_pos >= 0 ? substr(rest, 0, slash_pos) : rest;
let userinfo = "";
let hostport = authority;
let at_pos = rindex(authority, "@");
if (at_pos >= 0) {
userinfo = urldecode(substr(authority, 0, at_pos));
hostport = substr(authority, at_pos + 1);
}
let host_port = parse_host_port(hostport);
return {
scheme: scheme,
userinfo: userinfo,
host: host_port[0] || "",
port: host_port[1],
query: parse_query(query),
fragment: fragment
};
}
function normalize_utls_fingerprint(value) {
if (value == null || value == "")
return "";
let allowed = {
"": true,
chrome: true,
firefox: true,
edge: true,
safari: true,
"360": true,
ios: true,
android: true,
randomized: true,
randomizedalpn: true,
randomizednoalpn: true
};
value = as_string(value);
return allowed[value] ? value : "chrome";
}
function add_tls(url, security, default_tls) {
let query = object_or_empty(url.query);
let sni = query.sni || query.peer || "";
let insecure = query.allowInsecure || query.insecure || "";
let transport = query.type || "";
let alpn = tls_alpn_for_transport(split_csv(query.alpn || ""), transport);
let fingerprint = normalize_utls_fingerprint(query.fp || "");
let public_key = query.pbk || "";
let short_id = query.sid || "";
if (security == "reality" && public_key == "")
return [null, false];
if ((security == "reality" || public_key != "") && fingerprint == "")
fingerprint = "chrome";
let tls_enabled = false;
if (security == "tls" || security == "xtls" || security == "reality")
tls_enabled = true;
else if (security == null || security == "")
tls_enabled = default_tls || sni != "" || length(alpn) > 0 || fingerprint != "" || public_key != "";
if (!tls_enabled)
return [null, true];
let tls = { enabled: true };
if (sni != "")
tls.server_name = sni;
if (is_true(insecure))
tls.insecure = true;
if (length(alpn) > 0)
tls.alpn = alpn;
if (fingerprint != "")
tls.utls = { enabled: true, fingerprint: fingerprint };
if (security == "reality" || public_key != "") {
tls.reality = { enabled: true };
if (public_key != "")
tls.reality.public_key = public_key;
if (short_id != "")
tls.reality.short_id = short_id;
}
return [tls, true];
}
function add_transport(url) {
let query = object_or_empty(url.query);
let transport = query.type || "";
if (transport == "" || transport == "tcp")
return null;
let path = query.path || "";
let host = query.host || "";
let early_data = query.ed || "";
let grpc_service_name = query.serviceName || "";
let xhttp_mode = query.mode || "auto";
let sni = query.sni || "";
if (xhttp_mode != "auto" && xhttp_mode != "packet-up" && xhttp_mode != "stream-up" && xhttp_mode != "stream-one")
xhttp_mode = "auto";
if (transport == "ws") {
let result = { type: "ws", path: path != "" ? path : "/" };
if (host != "")
result.headers = { Host: host };
if (is_integer_string(early_data))
result.max_early_data = int(early_data);
return result;
}
if (transport == "grpc") {
let result = { type: "grpc" };
if (grpc_service_name != "")
result.service_name = grpc_service_name;
return result;
}
if (transport == "http" || transport == "h2") {
let result = { type: "http" };
if (path != "")
result.path = path;
if (host != "")
result.host = split_csv(host);
return result;
}
if (transport == "httpupgrade") {
let result = { type: "httpupgrade" };
if (path != "")
result.path = path;
if (host != "")
result.host = host;
return result;
}
if (transport == "xhttp") {
if (path == "")
path = "/";
if (host == "")
host = sni;
let result = {
type: "xhttp",
mode: xhttp_mode,
path: path,
x_padding_bytes: "100-1000",
no_grpc_header: false,
sc_max_each_post_bytes: "1000000",
sc_min_posts_interval_ms: "30"
};
if (host != "")
result.host = host;
let extra_settings = xhttp_extra_settings(query);
xhttp_optional_positive_range(result, "x_padding_bytes", xhttp_setting_value(query, extra_settings, "xPaddingBytes", "x_padding_bytes"));
xhttp_optional_bool(result, "no_grpc_header", xhttp_setting_value(query, extra_settings, "noGRPCHeader", "no_grpc_header"));
xhttp_optional_positive_range(result, "sc_max_each_post_bytes", xhttp_setting_value(query, extra_settings, "scMaxEachPostBytes", "sc_max_each_post_bytes"));
xhttp_optional_range(result, "sc_min_posts_interval_ms", xhttp_setting_value(query, extra_settings, "scMinPostsIntervalMs", "sc_min_posts_interval_ms"));
xhttp_optional_range(result, "sc_stream_up_server_secs", xhttp_setting_value(query, extra_settings, "scStreamUpServerSecs", "sc_stream_up_server_secs"));
let xmux = xhttp_normalize_xmux(xhttp_setting_value(query, extra_settings, "xmux", "xmux"));
if (xmux)
result.xmux = xmux;
return result;
}
return null;
}
function valid_port(port) {
let port_type = type(port);
return (port_type == "int" || port_type == "double") && port >= 1 && port <= 65535 && int(port) == port;
}
function normalize_port_number(value) {
value = trim(value);
if (!is_integer_string(value))
return "";
let port = int(value, 10);
if (port < 1 || port > 65535)
return "";
return "" + port;
}
function parse_hysteria2_server_ports(value) {
value = as_string(value);
if (index(value, ",") < 0 && index(value, "-") < 0)
return null;
let result = [];
for (let entry in split(value, ",")) {
entry = trim(entry);
if (entry == "")
return null;
let dash = index(entry, "-");
if (dash >= 0) {
if (index(substr(entry, dash + 1), "-") >= 0)
return null;
let start = normalize_port_number(substr(entry, 0, dash));
let end = normalize_port_number(substr(entry, dash + 1));
if (start == "" || end == "" || int(start, 10) > int(end, 10))
return null;
push(result, start + ":" + end);
}
else {
let port = normalize_port_number(entry);
if (port == "")
return null;
push(result, port + ":" + port);
}
}
return length(result) > 0 ? result : null;
}
function normalize_vless_encryption(value) {
value = as_string(value);
return value != "" && value != "none" ? value : "";
}
function process_vless(raw, url) {
if (url.host == "" || !valid_port(url.port) || url.userinfo == "")
return null;
let flow = url.query.flow || "";
if (flow != "" && flow != "xtls-rprx-vision")
return null;
let packet_encoding = url.query.packetEncoding || "";
if (packet_encoding != "xudp" && packet_encoding != "packetaddr")
packet_encoding = "";
let encryption = normalize_vless_encryption(url.query.encryption || "");
let outbound = {
type: "vless",
tag: url.fragment != "" ? url.fragment : (url.host + ":" + url.port),
share_link: raw,
server: url.host,
server_port: url.port,
uuid: url.userinfo
};
if (encryption != "")
outbound.encryption = encryption;
if (flow != "")
outbound.flow = flow;
if (packet_encoding != "")
outbound.packet_encoding = packet_encoding;
let tls_result = add_tls(url, url.query.security || "", false);
if (!tls_result[1])
return null;
if (tls_result[0])
outbound.tls = tls_result[0];
let transport = add_transport(url);
if (transport)
outbound.transport = transport;
return outbound;
}
function process_trojan(raw, url) {
if (url.host == "" || !valid_port(url.port) || url.userinfo == "")
return null;
let outbound = {
type: "trojan",
tag: url.fragment != "" ? url.fragment : (url.host + ":" + url.port),
share_link: raw,
server: url.host,
server_port: url.port,
password: url.userinfo
};
let tls_result = add_tls(url, url.query.security || "", true);
if (!tls_result[1])
return null;
if (tls_result[0])
outbound.tls = tls_result[0];
let transport = add_transport(url);
if (transport)
outbound.transport = transport;
return outbound;
}
function process_socks(raw, url) {
if (url.host == "" || !valid_port(url.port))
return null;
let username = "", password = "";
if (url.userinfo != "") {
let colon = index(url.userinfo, ":");
if (colon >= 0) {
username = urldecode(substr(url.userinfo, 0, colon));
password = urldecode(substr(url.userinfo, colon + 1));
if (username == password)
password = "";
}
else {
username = urldecode(url.userinfo);
}
}
let outbound = {
type: "socks",
tag: url.fragment != "" ? url.fragment : (url.host + ":" + url.port),
share_link: raw,
server: url.host,
server_port: url.port
};
let version = substr(url.scheme, 5);
if (version != "")
outbound.version = version;
if (username != "")
outbound.username = username;
if (password != "")
outbound.password = password;
return outbound;
}
function is_shadowsocks_userinfo_format(value) {
if (type(value) != "string")
return false;
let first = index(value, ":");
if (first <= 0 || first >= length(value) - 1)
return false;
let rest = substr(value, first + 1);
let second = index(rest, ":");
return second < 0 || index(substr(rest, second + 1), ":") < 0;
}
function process_shadowsocks(raw) {
if (!starts_with(raw, "ss://"))
return null;
let body = substr(raw, 5);
let fragment = "";
let hash_pos = index(body, "#");
if (hash_pos >= 0) {
fragment = urldecode(substr(body, hash_pos + 1));
body = substr(body, 0, hash_pos);
}
let query = "";
let question_pos = index(body, "?");
if (question_pos >= 0) {
query = substr(body, question_pos + 1);
body = substr(body, 0, question_pos);
}
let userinfo, hostport;
let at_pos = rindex(body, "@");
if (at_pos >= 0) {
userinfo = substr(body, 0, at_pos);
hostport = substr(body, at_pos + 1);
}
else {
let decoded = base64_decode(body);
if (!decoded)
return null;
at_pos = rindex(decoded, "@");
if (at_pos < 0)
return null;
userinfo = substr(decoded, 0, at_pos);
hostport = substr(decoded, at_pos + 1);
}
userinfo = urldecode(userinfo);
if (!is_shadowsocks_userinfo_format(userinfo)) {
let decoded = base64_decode(userinfo);
if (!decoded)
return null;
userinfo = decoded;
}
let cred_colon = index(userinfo, ":");
let host_port = parse_host_port(hostport);
if (cred_colon <= 0)
return null;
let method = substr(userinfo, 0, cred_colon);
let password = substr(userinfo, cred_colon + 1);
if (method == "" || method == "ss" || password == "" || host_port[0] == "" || !valid_port(host_port[1]))
return null;
let params = parse_query(query);
let plugin = params.plugin || "";
let plugin_opts = params["plugin-opts"] || "";
if (plugin != "" && plugin_opts == "") {
let parsed_plugin = match(plugin, /^([^;]+);(.*)$/);
if (parsed_plugin) {
plugin = parsed_plugin[1];
plugin_opts = parsed_plugin[2];
}
}
let outbound = {
type: "shadowsocks",
tag: fragment != "" ? fragment : (host_port[0] + ":" + host_port[1]),
share_link: raw,
server: host_port[0],
server_port: host_port[1],
method: method,
password: password
};
if (plugin != "")
outbound.plugin = plugin;
if (plugin_opts != "")
outbound.plugin_opts = plugin_opts;
return outbound;
}
function process_hysteria2(raw, url) {
let mport = trim(as_string(url.query.mport || ""));
let port_value = mport != "" ? mport : url.port;
let server_ports = parse_hysteria2_server_ports(port_value);
let server_port = normalize_port_number(as_string(port_value));
if (url.host == "" || (server_ports == null && server_port == "") || url.userinfo == "")
return null;
let password = url.userinfo;
if (password == "")
return null;
let tls = { enabled: true };
if ((url.query.sni || "") != "")
tls.server_name = url.query.sni;
if (is_true(url.query.allowInsecure || url.query.insecure))
tls.insecure = true;
if ((url.query.alpn || "") != "")
tls.alpn = split_csv(url.query.alpn);
let outbound = {
type: "hysteria2",
tag: url.fragment != "" ? url.fragment : (url.host + ":" + as_string(port_value)),
share_link: raw,
server: url.host,
password: password,
tls: tls
};
if (server_ports != null)
outbound.server_ports = server_ports;
else
outbound.server_port = int(server_port, 10);
if ((url.query.network || "") != "")
outbound.network = url.query.network;
if (is_integer_string(url.query.upmbps || ""))
outbound.up_mbps = int(url.query.upmbps);
if (is_integer_string(url.query.downmbps || ""))
outbound.down_mbps = int(url.query.downmbps);
if ((url.query.obfs || "") != "" && url.query.obfs != "none") {
outbound.obfs = { type: url.query.obfs };
if ((url.query["obfs-password"] || "") != "")
outbound.obfs.password = url.query["obfs-password"];
}
return outbound;
}
function process_tuic(raw, url) {
let userinfo = as_string(url.userinfo || "");
if (userinfo == "" || url.host == "" || url.port == "")
return null;
let uuid = "";
let password = "";
let colon_index = index(userinfo, ":");
if (colon_index >= 0) {
uuid = substr(userinfo, 0, colon_index);
password = substr(userinfo, colon_index + 1);
} else {
uuid = userinfo;
}
if (uuid == "")
return null;
let tls = { enabled: true };
if ((url.query.sni || "") != "")
tls.server_name = url.query.sni;
if (is_true(url.query.allowInsecure || url.query.insecure))
tls.insecure = true;
if ((url.query.alpn || "") != "")
tls.alpn = split_csv(url.query.alpn);
let outbound = {
type: "tuic",
tag: url.fragment != "" ? url.fragment : (url.host + ":" + url.port),
share_link: raw,
server: url.host,
server_port: int(url.port),
uuid: uuid,
tls: tls
};
if (password != "")
outbound.password = password;
let cc = as_string(url.query.congestion_control || "");
if (cc != "")
outbound.congestion_control = cc;
else
outbound.congestion_control = "bbr";
let udp_mode = as_string(url.query.udp_relay_mode || "");
if (udp_mode != "")
outbound.udp_relay_mode = udp_mode;
if (is_true(url.query.zero_rtt_handshake))
outbound.zero_rtt_handshake = true;
let heartbeat = as_string(url.query.heartbeat || "");
if (heartbeat != "")
outbound.heartbeat = heartbeat;
return outbound;
}
function string_value(value) {
return value == null ? "" : as_string(value);
}
function process_vmess_json(raw, decoded) {
let vmess = json_decode_text(decoded);
if (type(vmess) != "object")
return null;
let server = string_value(vmess.add);
let port = int(vmess.port || 0);
let uuid = string_value(vmess.id);
if (server == "" || !valid_port(port) || uuid == "")
return null;
let outbound = {
type: "vmess",
tag: string_value(vmess.ps) != "" ? string_value(vmess.ps) : (server + ":" + port),
share_link: raw,
server: server,
server_port: port,
uuid: uuid,
security: string_value(vmess.scy) != "" ? string_value(vmess.scy) : "auto"
};
let alter_id = int(vmess.aid || 0);
if (as_string(vmess.aid) != "")
outbound.alter_id = alter_id;
let network = string_value(vmess.net);
if (vmess.tls === true || vmess.tls == "tls" || vmess.tls == "true") {
let fingerprint = normalize_utls_fingerprint(string_value(vmess.fp));
let tls = { enabled: true };
if (string_value(vmess.sni) != "")
tls.server_name = string_value(vmess.sni);
let alpn = tls_alpn_for_transport(split_csv(string_value(vmess.alpn)), network);
if (length(alpn) > 0)
tls.alpn = alpn;
if (fingerprint != "")
tls.utls = { enabled: true, fingerprint: fingerprint };
outbound.tls = tls;
}
if (network == "ws") {
outbound.transport = {
type: "ws",
path: string_value(vmess.path) != "" ? string_value(vmess.path) : "/"
};
if (string_value(vmess.host) != "")
outbound.transport.headers = { Host: string_value(vmess.host) };
}
else if (network == "grpc") {
outbound.transport = { type: "grpc" };
if (string_value(vmess.path) != "")
outbound.transport.service_name = string_value(vmess.path);
}
else if (network == "http" || network == "h2") {
outbound.transport = { type: "http" };
if (string_value(vmess.path) != "")
outbound.transport.path = string_value(vmess.path);
if (string_value(vmess.host) != "")
outbound.transport.host = split_csv(string_value(vmess.host));
}
return outbound;
}
function process_vmess(raw) {
if (!starts_with(raw, "vmess://"))
return null;
let encoded = substr(raw, 8);
let hash_pos = index(encoded, "#");
if (hash_pos >= 0)
encoded = substr(encoded, 0, hash_pos);
let decoded = base64_decode(encoded);
if (!decoded)
return null;
if (index(decoded, "\r") >= 0 || index(decoded, "\n") >= 0)
decoded = replace(decoded, /[\r\n]/g, "");
let trimmed = trim(decoded);
if (substr(trimmed, 0, 1) != "{" || substr(trimmed, length(trimmed) - 1) != "}")
return null;
return process_vmess_json(raw, trimmed);
}
function parse_share_link(line) {
if (starts_with(line, "vmess://"))
return process_vmess(line);
if (starts_with(line, "ss://"))
return process_shadowsocks(line);
let url = parse_url(line);
if (!url)
return null;
if (url.scheme == "vless")
return process_vless(line, url);
if (url.scheme == "trojan")
return process_trojan(line, url);
if (url.scheme == "hysteria2" || url.scheme == "hy2")
return process_hysteria2(line, url);
if (url.scheme == "tuic")
return process_tuic(line, url);
if (match(url.scheme, /^socks/))
return process_socks(line, url);
return null;
}
function share_link_outbound(raw, tag) {
let outbound = parse_share_link(raw);
if (type(outbound) != "object")
return false;
outbound.tag = as_string(tag);
delete outbound.share_link;
delete outbound.remark;
write_json(outbound);
return true;
}
function clean_scalar(value) {
value = trim(value);
let first = substr(value, 0, 1);
let last = substr(value, length(value) - 1);
if (length(value) >= 2 && ((first == "\"" && last == "\"") || (first == "'" && last == "'")))
return substr(value, 1, length(value) - 2);
return value;
}
function leading_indent(value) {
let m = match(as_string(value), /^[ \t]*/);
return m ? length(m[0]) : 0;
}
function find_top_level_colon(value) {
let depth = 0, quote = "", escaped = false;
for (let i = 0; i < length(value); i++) {
let char = substr(value, i, 1);
if (quote != "") {
if (escaped)
escaped = false;
else if (char == "\\")
escaped = true;
else if (char == quote)
quote = "";
}
else if (char == "\"" || char == "'")
quote = char;
else if (char == "{" || char == "[")
depth++;
else if (char == "}" || char == "]")
depth--;
else if (char == ":" && depth == 0)
return i;
}
return null;
}
function split_top_level(value, separator) {
let result = [], depth = 0, quote = "", escaped = false, start = 0;
for (let i = 0; i < length(value); i++) {
let char = substr(value, i, 1);
if (quote != "") {
if (escaped)
escaped = false;
else if (char == "\\")
escaped = true;
else if (char == quote)
quote = "";
}
else if (char == "\"" || char == "'")
quote = char;
else if (char == "{" || char == "[")
depth++;
else if (char == "}" || char == "]")
depth--;
else if (char == separator && depth == 0) {
push(result, substr(value, start, i - start));
start = i + 1;
}
}
push(result, substr(value, start));
return result;
}
function set_record_field(record, key, value) {
key = clean_scalar(key);
value = clean_scalar(value);
if (key == "")
return;
if (record.fields[key] == null)
push(record.keys, key);
record.fields[key] = value;
}
let parse_clash_map;
function parse_clash_pair(record, part, prefix) {
part = trim(part);
let colon = find_top_level_colon(part);
if (colon == null)
return;
let key = clean_scalar(substr(part, 0, colon));
let value = trim(substr(part, colon + 1));
if (substr(value, 0, 1) == "{" && substr(value, length(value) - 1) == "}")
parse_clash_map(record, value, prefix + key + ".");
else
set_record_field(record, prefix + key, value);
}
parse_clash_map = function(record, value, prefix) {
value = trim(value);
if (substr(value, 0, 1) == "{")
value = substr(value, 1);
if (substr(value, length(value) - 1) == "}")
value = substr(value, 0, length(value) - 1);
for (let part in split_top_level(value, ","))
parse_clash_pair(record, part, prefix);
};
function empty_clash_record() {
return {
fields: {},
keys: [],
context: "",
context_indent: -1
};
}
function clash_record_has_fields(record) {
return length(record.keys) > 0;
}
function emit_clash_record(records, record) {
if (!clash_record_has_fields(record))
return empty_clash_record();
let item = {};
for (let key in record.keys) {
if (record.fields[key] != null)
item[key] = record.fields[key];
}
push(records, item);
return empty_clash_record();
}
let clash_nested_keys = {
"ws-opts": true,
"grpc-opts": true,
"reality-opts": true,
"obfs-opts": true,
headers: true
};
function parse_clash_block_line(record, line) {
let indent = leading_indent(line);
let text = trim(line);
if (text == "")
return;
let colon = index(text, ":");
if (colon < 0)
return;
let key = clean_scalar(substr(text, 0, colon));
let value = trim(substr(text, colon + 1));
if (record.context != "" && indent <= record.context_indent) {
record.context = "";
record.context_indent = -1;
}
if (value == "") {
if (clash_nested_keys[key]) {
if (record.context != "" && indent > record.context_indent)
record.context = record.context + "." + key;
else
record.context = key;
record.context_indent = indent;
}
return;
}
let full_key = key;
if (record.context != "" && indent > record.context_indent)
full_key = record.context + "." + key;
set_record_field(record, full_key, value);
}
function is_clash_proxies_header(line) {
let text = trim(line);
return text == "proxies:" || match(text, /^proxies:\s*#/);
}
function clash_yaml_records(input_file) {
let data = read_file(input_file);
if (data == null)
return [];
let records = [];
let record = empty_clash_record();
let in_proxies = false;
for (let line in split(data, "\n")) {
line = replace(line, /\r$/g, "");
let proxies_header = is_clash_proxies_header(line);
if (in_proxies && match(line, /^[^ \t]/) && !proxies_header && !match(line, /^-[ \t]/)) {
record = emit_clash_record(records, record);
in_proxies = false;
}
if (proxies_header) {
record = emit_clash_record(records, record);
in_proxies = true;
}
else if (in_proxies) {
if (match(line, /^[ \t]*-[ \t]*\{/)) {
record = emit_clash_record(records, record);
record = empty_clash_record();
let map_value = replace(line, /^[ \t]*-[ \t]*/g, "");
parse_clash_map(record, map_value, "");
record = emit_clash_record(records, record);
}
else if (match(line, /^[ \t]*-[ \t]*/)) {
record = emit_clash_record(records, record);
record = empty_clash_record();
let rest = replace(line, /^[ \t]*-[ \t]*/g, "");
if (trim(rest) != "")
parse_clash_block_line(record, rest);
}
else if (clash_record_has_fields(record)) {
parse_clash_block_line(record, line);
}
}
}
emit_clash_record(records, record);
return records;
}
function normalize_packet_encoding(value) {
value = as_string(value);
return value == "xudp" || value == "packetaddr" ? value : "";
}
function clash_vless_flow_supported(flow) {
return flow == null || flow == "" || flow == "xtls-rprx-vision";
}
function normalized_clash_alpn(value) {
return replace(replace(replace(as_string(value), /^\[/g, ""), /\]$/g, ""), /\s+/g, "");
}
function add_clash_tls(outbound, options) {
let enabled = options.always || is_true(options.tls) || options.sni != "" ||
length(tls_alpn_for_transport(split_csv(options.alpn), options.network)) > 0 ||
options.fingerprint != "" || options.reality_public_key != "";
if (!enabled)
return;
let tls = { enabled: true };
if (options.sni != "")
tls.server_name = options.sni;
if (is_true(options.skip_verify))
tls.insecure = true;
let alpn = tls_alpn_for_transport(split_csv(options.alpn), options.network);
if (length(alpn) > 0)
tls.alpn = alpn;
if (options.reality_public_key != "") {
tls.utls = {
enabled: true,
fingerprint: options.fingerprint != "" ? options.fingerprint : "chrome"
};
tls.reality = { enabled: true, public_key: options.reality_public_key };
if (options.reality_short_id != "")
tls.reality.short_id = options.reality_short_id;
}
else if (options.fingerprint != "") {
tls.utls = { enabled: true, fingerprint: options.fingerprint };
}
outbound.tls = tls;
}
function add_clash_transport(outbound, options) {
if (options.network == "ws") {
outbound.transport = {
type: "ws",
path: options.ws_path != "" ? options.ws_path : "/"
};
if (options.ws_host != "")
outbound.transport.headers = { Host: options.ws_host };
}
else if (options.network == "grpc") {
outbound.transport = { type: "grpc" };
if (options.grpc_service_name != "")
outbound.transport.service_name = options.grpc_service_name;
}
}
function parse_clash_record(record) {
let proxy_type = lc(as_string(record.type));
let name = as_string(record.name);
let server = as_string(record.server);
let port = int(record.port || 0);
if (name == "")
name = server + ":" + as_string(record.port);
if (proxy_type == "" || server == "" || !valid_port(port))
return null;
let options = {
tls: as_string(record.tls),
skip_verify: as_string(record["skip-cert-verify"]),
sni: as_string(record.sni || record.servername),
network: lc(as_string(record.network)),
ws_path: as_string(record["ws-opts.path"]),
ws_host: as_string(record["ws-opts.headers.Host"]),
grpc_service_name: as_string(record["grpc-opts.grpc-service-name"]),
reality_public_key: as_string(record["reality-opts.public-key"]),
reality_short_id: as_string(record["reality-opts.short-id"]),
alpn: normalized_clash_alpn(record.alpn || ""),
fingerprint: normalize_utls_fingerprint(as_string(record["client-fingerprint"] || record.fingerprint))
};
if (proxy_type == "ss" || proxy_type == "shadowsocks") {
let method = as_string(record.cipher);
let password = as_string(record.password);
if (method == "" || method == "ss" || password == "")
return null;
return { type: "shadowsocks", tag: name, server: server, server_port: port, method: method, password: password };
}
if (proxy_type == "vmess") {
let uuid = as_string(record.uuid);
if (uuid == "")
return null;
let outbound = {
type: "vmess",
tag: name,
server: server,
server_port: port,
uuid: uuid,
security: as_string(record.cipher) != "" ? as_string(record.cipher) : "auto"
};
if (as_string(record.alterId || record["alter-id"]) != "")
outbound.alter_id = int(record.alterId || record["alter-id"]);
add_clash_tls(outbound, options);
add_clash_transport(outbound, options);
return outbound;
}
if (proxy_type == "vless") {
let uuid = as_string(record.uuid);
let flow = as_string(record.flow);
let packet_encoding = normalize_packet_encoding(record["packet-encoding"] || record.packetEncoding || "");
let encryption = normalize_vless_encryption(record.encryption || "");
if (uuid == "" || !clash_vless_flow_supported(flow))
return null;
let outbound = { type: "vless", tag: name, server: server, server_port: port, uuid: uuid };
if (encryption != "")
outbound.encryption = encryption;
if (flow != "")
outbound.flow = flow;
if (packet_encoding != "")
outbound.packet_encoding = packet_encoding;
add_clash_tls(outbound, options);
add_clash_transport(outbound, options);
return outbound;
}
if (proxy_type == "trojan") {
let password = as_string(record.password);
if (password == "")
return null;
let outbound = { type: "trojan", tag: name, server: server, server_port: port, password: password };
options.always = true;
add_clash_tls(outbound, options);
add_clash_transport(outbound, options);
return outbound;
}
if (proxy_type == "hysteria2" || proxy_type == "hy2") {
let password = as_string(record.password);
if (password == "")
return null;
let outbound = { type: "hysteria2", tag: name, server: server, server_port: port, password: password, tls: { enabled: true } };
if (options.sni != "")
outbound.tls.server_name = options.sni;
if (is_true(options.skip_verify))
outbound.tls.insecure = true;
if (options.alpn != "")
outbound.tls.alpn = split_csv(options.alpn);
let obfs = as_string(record.obfs);
let obfs_password = as_string(record["obfs-password"] || record["obfs-opts.password"]);
if (obfs != "" && obfs != "none") {
outbound.obfs = { type: obfs };
if (obfs_password != "")
outbound.obfs.password = obfs_password;
}
return outbound;
}
if (proxy_type == "tuic") {
let uuid = as_string(record.uuid || "");
let password = as_string(record.password || "");
if (uuid == "")
return null;
let outbound = { type: "tuic", tag: name, server: server, server_port: port, uuid: uuid, tls: { enabled: true } };
if (password != "")
outbound.password = password;
if (options.sni != "")
outbound.tls.server_name = options.sni;
if (is_true(options.skip_verify) || is_true(record["skip-cert-verify"]))
outbound.tls.insecure = true;
if (options.alpn != "")
outbound.tls.alpn = split_csv(options.alpn);
let cc = as_string(record["congestion-control"] || record.congestion_control || "");
if (cc != "")
outbound.congestion_control = cc;
let udp_mode = as_string(record["udp-relay-mode"] || record.udp_relay_mode || "");
if (udp_mode != "")
outbound.udp_relay_mode = udp_mode;
if (is_true(record["zero-rtt-握手"]) || is_true(record["zero_rtt_handshake"]))
outbound.zero_rtt_handshake = true;
return outbound;
}
if (proxy_type == "socks5" || proxy_type == "socks") {
let outbound = { type: "socks", tag: name, server: server, server_port: port, version: "5" };
if (as_string(record.username) != "")
outbound.username = as_string(record.username);
if (as_string(record.password) != "")
outbound.password = as_string(record.password);
return outbound;
}
return null;
}
function normalize_clash_yaml(input_file, output_file) {
let outbounds = [];
let skipped = 0;
for (let record in clash_yaml_records(input_file)) {
let outbound = parse_clash_record(record);
if (outbound)
push(outbounds, outbound);
else
skipped++;
}
if (length(outbounds) == 0) {
fs.unlink(output_file);
return false;
}
return write_json_file(output_file, {
version: 1,
format: "clash-yaml",
skipped: skipped,
outbounds: outbounds
});
}
function normalize_uri_list_data(data, output_file) {
data = as_string(data);
if (index(data, "\r") >= 0)
data = replace(data, /\r/g, "");
let output = fs.open(output_file, "w");
if (!output)
return false;
let added = 0;
let skipped = 0;
output.write("{\"version\":1,\"format\":\"uri-list\",\"outbounds\":[");
for (let line in split(data, "\n")) {
if (index(line, "\r") >= 0)
line = replace(line, /\r/g, "");
line = trim(line);
if (line != "" && !starts_with(line, "#")) {
let outbound = parse_share_link(line);
if (outbound) {
if (added > 0)
output.write(",");
output.write(sprintf("%J", outbound));
added++;
}
else {
skipped++;
}
}
}
output.write("],\"skipped\":" + skipped + "}\n");
output.close();
if (added == 0) {
fs.unlink(output_file);
return false;
}
return true;
}
let metadata_allowed_keys = {
"profile-title": true,
"subscription-userinfo": true,
"profile-web-page-url": true,
"support-url": true,
"announce": true,
"announce-url": true,
"subscription-refill-date": true,
"content-disposition": true
};
function metadata_new_store() {
return {
values: {},
seen: {},
order: []
};
}
function write_metadata_compact_json(store) {
print("{");
for (let i = 0; i < length(store.order); i++) {
let key = store.order[i];
if (i > 0)
print(",");
print(sprintf("%J", key), ":", sprintf("%J", store.values[key]));
}
print("}\n");
}
function add_metadata_entry(store, key, value) {
key = lc(trim(key));
value = replace(trim(value), /\r/g, "");
if (!metadata_allowed_keys[key] || value == "")
return;
if (!store.seen[key]) {
push(store.order, key);
store.seen[key] = true;
}
store.values[key] = value;
}
function parse_metadata_line(store, line) {
line = trim(line);
let colon = index(line, ":");
if (colon <= 0)
return;
add_metadata_entry(store, substr(line, 0, colon), substr(line, colon + 1));
}
function metadata_headers_store(input_file) {
let data = read_file(input_file);
let store = metadata_new_store();
if (data != null) {
for (let line in split(data, "\n"))
parse_metadata_line(store, line);
}
return store;
}
function metadata_headers_json(input_file) {
write_metadata_compact_json(metadata_headers_store(input_file));
return true;
}
function metadata_body_store_from_data(data) {
let store = metadata_new_store();
let line_no = 0;
if (data != null) {
for (let line in split(data, "\n")) {
line_no++;
if (line_no > 20)
break;
line = trim(line);
if (starts_with(line, "#"))
line = replace(line, /^#[ \t]*/g, "");
else if (starts_with(line, "//"))
line = replace(line, /^\/\/[ \t]*/g, "");
else
continue;
parse_metadata_line(store, line);
}
}
return store;
}
function metadata_body_store(input_file) {
return metadata_body_store_from_data(read_file(input_file));
}
function metadata_body_store_with_base64_fallback(input_file) {
let store = metadata_body_store(input_file);
if (length(store.order) > 0)
return store;
let data = read_file(input_file);
if (data == null)
return store;
let decoded = base64_decode(replace(as_string(data), /[\r\n\t ]/g, ""));
if (decoded == null || decoded == "")
return store;
return metadata_body_store_from_data(decoded);
}
function metadata_body_json(input_file) {
write_metadata_compact_json(metadata_body_store(input_file));
return true;
}
function metadata_trim_text(value) {
return trim(as_string(value));
}
function metadata_clean_text(value, max, decode_base64) {
value = as_string(value);
if (decode_base64 && lc(substr(value, 0, 7)) == "base64:") {
let decoded = base64_decode(substr(value, 7));
value = decoded == null ? "" : decoded;
}
let cleaned = trim(replace(replace(value, regexp('[\\x00-\\x1f\\x7f]', 'g'), " "), regexp(' +', 'g'), " "));
if (cleaned == "")
return null;
return length(cleaned) > max ? substr(cleaned, 0, max) : cleaned;
}
function metadata_has_control_or_space(value) {
return match(as_string(value), regexp('[\\x00-\\x20\\x7f]')) != null;
}
function metadata_clean_url(value) {
let cleaned = metadata_trim_text(value);
if (cleaned == "" || length(cleaned) > 2048)
return null;
if (!starts_with(cleaned, "http://") && !starts_with(cleaned, "https://"))
return null;
return metadata_has_control_or_space(cleaned) ? null : cleaned;
}
function metadata_clean_number(value) {
value = metadata_trim_text(value);
if (value == "" || value == null)
return null;
return match(value, /^[0-9]+$/) != null ? value : null;
}
function metadata_replace_path_separators(value) {
return replace(as_string(value), /[\/\\]/g, "_");
}
function metadata_content_disposition_filename(value) {
value = as_string(value);
let filename = null;
let quoted_marker = "filename=\"";
let quoted_pos = index(value, quoted_marker);
if (quoted_pos >= 0) {
let rest = substr(value, quoted_pos + length(quoted_marker));
let quote_end = index(rest, "\"");
filename = quote_end >= 0 ? substr(rest, 0, quote_end) : rest;
}
else {
let marker = "filename=";
let pos = index(value, marker);
if (pos < 0)
return null;
let rest = substr(value, pos + length(marker));
let semicolon = index(rest, ";");
filename = semicolon >= 0 ? substr(rest, 0, semicolon) : rest;
if (length(filename) > 0 && substr(filename, 0, 1) == "\"")
filename = substr(filename, 1);
if (length(filename) > 0 && substr(filename, length(filename) - 1) == "\"")
filename = substr(filename, 0, length(filename) - 1);
}
filename = metadata_clean_text(filename, 120, false);
if (filename == null)
return null;
filename = metadata_replace_path_separators(filename);
return filename == "" ? null : filename;
}
function metadata_number_value(value) {
return value == null ? 0 : int(value, 10);
}
function metadata_parse_userinfo(value) {
let result = {};
for (let item in split(as_string(value), ";")) {
item = trim(item);
let equals = index(item, "=");
if (equals < 0)
continue;
let key = lc(trim(substr(item, 0, equals)));
let number = metadata_clean_number(substr(item, equals + 1));
if (number == null)
continue;
if (key == "upload" || key == "download" || key == "total" || key == "expire")
result[key] = number;
}
return result;
}
function metadata_ui_object(raw) {
raw = object_or_empty(raw);
let title = metadata_clean_text(raw["profile-title"], 120, true);
let web_page_url = metadata_clean_url(raw["profile-web-page-url"]);
let support_url = metadata_clean_url(raw["support-url"]);
let announce = metadata_clean_text(raw["announce"], 500, true);
let announce_url = metadata_clean_url(raw["announce-url"]);
let refill_date = metadata_clean_number(raw["subscription-refill-date"]);
let file_name = metadata_content_disposition_filename(raw["content-disposition"]);
let userinfo = metadata_parse_userinfo(raw["subscription-userinfo"]);
let upload = userinfo.upload;
let download = userinfo.download;
let total = userinfo.total;
let expire = userinfo.expire;
let has_traffic = upload != null || download != null || total != null || expire != null;
let used = metadata_number_value(upload) + metadata_number_value(download);
let total_value = metadata_number_value(total);
let result = { version: 1 };
if (title != null)
result.title = title;
if (has_traffic) {
let traffic = {
used: used,
isUnlimited: !(total != null && total_value > 0)
};
if (upload != null)
traffic.upload = metadata_number_value(upload);
if (download != null)
traffic.download = metadata_number_value(download);
if (total != null && total_value > 0) {
traffic.total = total_value;
let remaining = total_value - used;
traffic.remaining = remaining < 0 ? 0 : remaining;
}
result.traffic = traffic;
}
if (expire != null && metadata_number_value(expire) > 0)
result.expire = metadata_number_value(expire);
if (refill_date != null && metadata_number_value(refill_date) > 0)
result.refillDate = metadata_number_value(refill_date);
if (web_page_url != null)
result.webPageUrl = web_page_url;
if (support_url != null)
result.supportUrl = support_url;
if (announce != null)
result.announce = announce;
if (announce_url != null)
result.announceUrl = announce_url;
if (file_name != null)
result.fileName = file_name;
return result;
}
function metadata_ui_json(raw_path) {
let result = metadata_ui_object(read_json_file(raw_path));
if (length(keys(result)) > 1)
write_json(result);
return true;
}
function metadata_extract_ui_json(headers_file, body_file) {
let raw = metadata_body_store_with_base64_fallback(body_file).values;
let headers = metadata_headers_store(headers_file).values;
for (let key, value in headers)
raw[key] = value;
let result = metadata_ui_object(raw);
if (length(keys(result)) > 1)
write_json(result);
return true;
}
function is_metadata_preamble_line(line) {
let trimmed = trim(line);
let m = match(trimmed, /^(#|\/\/)[ \t]*([A-Za-z0-9][A-Za-z0-9_-]*)[ \t]*:/);
return m && metadata_allowed_keys[lc(m[2])];
}
function normalize_uri_list_stream(input, output_file, strip_metadata) {
let output = fs.open(output_file, "w");
if (!output)
return false;
let added = 0;
let skipped = 0;
let line_no = 0;
output.write("{\"version\":1,\"format\":\"uri-list\",\"outbounds\":[");
while (true) {
let line = input.read("line");
if (line == null)
break;
line_no++;
line = trim(line);
if (strip_metadata && line_no <= 20 && is_metadata_preamble_line(line))
continue;
if (line != "" && !starts_with(line, "#")) {
let outbound = parse_share_link(line);
if (outbound) {
if (added > 0)
output.write(",");
output.write(sprintf("%J", outbound));
added++;
}
else {
skipped++;
}
}
}
output.write("],\"skipped\":" + skipped + "}\n");
output.close();
if (added == 0) {
fs.unlink(output_file);
return false;
}
return true;
}
function file_looks_like_uri_list(input_file) {
let input = fs.open(input_file, "r");
if (!input)
return false;
let line_no = 0;
while (line_no < 200) {
let line = input.read("line");
if (line == null)
break;
line_no++;
line = trim(line);
if (line == "" || starts_with(line, "#") || is_metadata_preamble_line(line))
continue;
if (is_supported_share_link(line)) {
input.close();
return true;
}
if (substr(line, 0, 1) == "{" || substr(line, 0, 1) == "[" || index(line, "proxies:") >= 0) {
input.close();
return false;
}
}
input.close();
return false;
}
function normalize_uri_list_file(input_file, output_file, strip_metadata) {
let input = fs.open(input_file, "r");
if (!input)
return false;
let ok = normalize_uri_list_stream(input, output_file, strip_metadata);
input.close();
return ok;
}
function normalize_uri_list(input_file, output_file) {
return normalize_uri_list_file(input_file, output_file, false);
}
function strip_metadata_preamble_data(data) {
data = as_string(data);
let offset = 0;
let changed = false;
for (let line_no = 1; line_no <= 20 && offset <= length(data); line_no++) {
let rest = substr(data, offset);
let newline_pos = index(rest, "\n");
let line = newline_pos >= 0 ? substr(rest, 0, newline_pos) : rest;
if (is_metadata_preamble_line(line)) {
changed = true;
break;
}
if (newline_pos < 0)
break;
offset += newline_pos + 1;
}
if (!changed)
return data;
let result = [];
let line_no = 0;
for (let line in split(data, "\n")) {
line_no++;
let text = replace(line, /\r$/g, "");
if (line_no <= 20) {
if (is_metadata_preamble_line(text))
continue;
}
push(result, text);
}
return join("\n", result);
}
function content_is_clash_yaml(data) {
for (let line in split(data, "\n")) {
if (match(line, /^[ \t]*proxies:[ \t]*(#.*)?$/))
return true;
}
return false;
}
function xray_first_array_object(value) {
value = array_or_empty(value);
return length(value) > 0 && type(value[0]) == "object" ? value[0] : {};
}
function xray_first_string(values) {
for (let value in values) {
value = as_string(value);
if (value != "")
return value;
}
return "";
}
function xray_valid_port_value(value) {
let value_type = type(value);
if (value_type == "int" || value_type == "double")
return valid_port(value) ? int(value) : null;
value = as_string(value);
if (!is_integer_string(value))
return null;
let port = int(value, 10);
return valid_port(port) ? port : null;
}
function xray_alpn(value) {
if (type(value) == "array")
return value;
let text = as_string(value);
return text != "" ? split_csv(text) : [];
}
function xray_add_utls(tls, fingerprint) {
fingerprint = normalize_utls_fingerprint(fingerprint);
if (fingerprint != "")
tls.utls = { enabled: true, fingerprint: fingerprint };
}
function xray_tls_from_stream(stream, network) {
stream = object_or_empty(stream);
let security = lc(as_string(stream.security || ""));
let reality = object_or_empty(stream.realitySettings);
let tls_settings = object_or_empty(stream.tlsSettings);
let settings = security == "reality" ? reality : tls_settings;
if (security != "tls" && security != "xtls" && security != "reality" && length(keys(settings)) == 0)
return null;
let tls = { enabled: true };
let server_name = xray_first_string([settings.serverName, settings.server_name, settings.sni]);
let alpn = tls_alpn_for_transport(xray_alpn(settings.alpn), network);
if (server_name != "")
tls.server_name = server_name;
if (is_true(settings.allowInsecure) || is_true(settings.insecure))
tls.insecure = true;
if (length(alpn) > 0)
tls.alpn = alpn;
xray_add_utls(tls, settings.fingerprint || settings.fp || "");
if (security == "reality" || length(keys(reality)) > 0) {
tls.reality = { enabled: true };
let public_key = xray_first_string([reality.publicKey, reality.public_key, settings.publicKey, settings.public_key]);
let short_id = xray_first_string([reality.shortId, reality.short_id, settings.shortId, settings.short_id]);
if (public_key != "")
tls.reality.public_key = public_key;
if (short_id != "")
tls.reality.short_id = short_id;
if (!tls.utls)
tls.utls = { enabled: true, fingerprint: "chrome" };
}
return tls;
}
function xray_transport_from_stream(stream) {
stream = object_or_empty(stream);
let network = lc(as_string(stream.network || ""));
if (network == "" || network == "tcp")
return null;
if (network == "ws") {
let settings = object_or_empty(stream.wsSettings);
let headers = object_or_empty(settings.headers);
let result = { type: "ws", path: as_string(settings.path || "/") };
let host = as_string(headers.Host || headers.host || "");
if (host != "")
result.headers = { Host: host };
return result;
}
if (network == "grpc") {
let settings = object_or_empty(stream.grpcSettings);
let result = { type: "grpc" };
if (as_string(settings.serviceName || settings.service_name || "") != "")
result.service_name = as_string(settings.serviceName || settings.service_name);
return result;
}
if (network == "http" || network == "h2") {
let settings = object_or_empty(stream.httpSettings);
let result = { type: "http" };
if (type(settings.path) == "array" && length(settings.path) > 0)
result.path = as_string(settings.path[0]);
else if (as_string(settings.path || "") != "")
result.path = as_string(settings.path);
if (type(settings.host) == "array")
result.host = settings.host;
else if (as_string(settings.host || "") != "")
result.host = split_csv(settings.host);
return result;
}
if (network == "httpupgrade") {
let settings = object_or_empty(stream.httpupgradeSettings);
let result = { type: "httpupgrade" };
if (as_string(settings.path || "") != "")
result.path = as_string(settings.path);
if (as_string(settings.host || "") != "")
result.host = as_string(settings.host);
return result;
}
if (network == "xhttp") {
let settings = object_or_empty(stream.xhttpSettings);
let result = {
type: "xhttp",
mode: as_string(settings.mode || "auto"),
path: as_string(settings.path || "/"),
x_padding_bytes: "100-1000",
no_grpc_header: false,
sc_max_each_post_bytes: "1000000",
sc_min_posts_interval_ms: "30"
};
if (result.mode != "auto" && result.mode != "packet-up" && result.mode != "stream-up" && result.mode != "stream-one")
result.mode = "auto";
if (as_string(settings.host || "") != "")
result.host = as_string(settings.host);
xhttp_optional_positive_range(result, "x_padding_bytes", xhttp_setting_value(settings, settings, "xPaddingBytes", "x_padding_bytes"));
xhttp_optional_bool(result, "no_grpc_header", xhttp_setting_value(settings, settings, "noGRPCHeader", "no_grpc_header"));
xhttp_optional_positive_range(result, "sc_max_each_post_bytes", xhttp_setting_value(settings, settings, "scMaxEachPostBytes", "sc_max_each_post_bytes"));
xhttp_optional_range(result, "sc_min_posts_interval_ms", xhttp_setting_value(settings, settings, "scMinPostsIntervalMs", "sc_min_posts_interval_ms"));
xhttp_optional_range(result, "sc_stream_up_server_secs", xhttp_setting_value(settings, settings, "scStreamUpServerSecs", "sc_stream_up_server_secs"));
let xmux = xhttp_normalize_xmux(settings.xmux);
if (xmux)
result.xmux = xmux;
return result;
}
return null;
}
function xray_display_name(original_tag) {
original_tag = as_string(original_tag);
return original_tag;
}
function xray_config_display_name(config, fallback) {
let name = xray_first_string([config.remarks, config.remark, config.name, config.ps]);
return name != "" ? name : fallback;
}
function xray_source_tag(outbound, fallback) {
let tag = as_string(outbound.tag || "");
return tag != "" ? tag : fallback;
}
function xray_unique_tag(base, taken) {
base = as_string(base);
if (base == "")
base = "server";
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 xray_tag_base(original_tag, config_index, config_count) {
original_tag = as_string(original_tag);
if (original_tag == "")
original_tag = "server";
return original_tag;
}
function xray_protocol_supported(protocol) {
protocol = lc(as_string(protocol));
return protocol == "vless" || protocol == "socks" || protocol == "hysteria";
}
function convert_xray_vless(outbound, tag) {
let vnext = xray_first_array_object(object_or_empty(outbound.settings).vnext);
let user = xray_first_array_object(vnext.users);
let server = as_string(vnext.address || "");
let port = xray_valid_port_value(vnext.port);
let uuid = as_string(user.id || "");
if (server == "" || port == null || uuid == "")
return null;
let result = {
type: "vless",
tag: tag,
server: server,
server_port: port,
uuid: uuid
};
let flow = as_string(user.flow || "");
if (flow != "")
result.flow = flow;
let encryption = normalize_vless_encryption(user.encryption || "");
if (encryption != "")
result.encryption = encryption;
let stream = object_or_empty(outbound.streamSettings);
let transport = xray_transport_from_stream(stream);
let tls = xray_tls_from_stream(stream, lc(as_string(stream.network || "")));
if (tls)
result.tls = tls;
if (transport)
result.transport = transport;
return result;
}
function convert_xray_socks(outbound, tag) {
let server_config = xray_first_array_object(object_or_empty(outbound.settings).servers);
let server = as_string(server_config.address || "");
let port = xray_valid_port_value(server_config.port);
if (server == "" || port == null)
return null;
let result = {
type: "socks",
tag: tag,
server: server,
server_port: port,
version: "5"
};
let user = xray_first_array_object(server_config.users);
if (as_string(user.user || "") != "")
result.username = as_string(user.user);
if (as_string(user.pass || user.password || "") != "")
result.password = as_string(user.pass || user.password);
return result;
}
function convert_xray_hysteria2(outbound, tag) {
let settings = object_or_empty(outbound.settings);
let stream = object_or_empty(outbound.streamSettings);
let hysteria_settings = object_or_empty(stream.hysteriaSettings);
let version = int(settings.version || hysteria_settings.version || 0);
if (version != 2)
return null;
let server = as_string(settings.address || settings.server || "");
let port = xray_valid_port_value(settings.port);
let password = as_string(hysteria_settings.auth || settings.auth || settings.password || "");
if (server == "" || port == null || password == "")
return null;
let result = {
type: "hysteria2",
tag: tag,
server: server,
server_port: port,
password: password,
tls: { enabled: true }
};
let tls_settings = object_or_empty(stream.tlsSettings);
let server_name = xray_first_string([tls_settings.serverName, tls_settings.server_name, tls_settings.sni]);
if (server_name != "")
result.tls.server_name = server_name;
if (is_true(tls_settings.allowInsecure) || is_true(tls_settings.insecure))
result.tls.insecure = true;
let alpn = xray_alpn(tls_settings.alpn);
if (length(alpn) > 0)
result.tls.alpn = alpn;
return result;
}
function convert_xray_outbound(outbound, tag) {
let protocol = lc(as_string(outbound.protocol || ""));
if (protocol == "vless")
return convert_xray_vless(outbound, tag);
if (protocol == "socks")
return convert_xray_socks(outbound, tag);
if (protocol == "hysteria")
return convert_xray_hysteria2(outbound, tag);
return null;
}
function xray_outbound_detour(outbound) {
let stream = object_or_empty(outbound.streamSettings);
let sockopt = object_or_empty(stream.sockopt);
if (as_string(sockopt.dialerProxy || "") != "")
return as_string(sockopt.dialerProxy);
sockopt = object_or_empty(outbound.sockopt);
return as_string(sockopt.dialerProxy || "");
}
function xray_supported_source_tags(source_outbounds) {
let result = {};
for (let i = 0; i < length(source_outbounds); i++) {
let outbound = source_outbounds[i];
if (type(outbound) != "object" || !xray_protocol_supported(outbound.protocol))
continue;
let tag = xray_source_tag(outbound, lc(as_string(outbound.protocol || "server")) + "-" + (i + 1));
result[tag] = true;
}
return result;
}
function xray_primary_source_tag(config, source_outbounds) {
let supported_tags = xray_supported_source_tags(source_outbounds);
for (let rule in array_or_empty(object_or_empty(config.routing).rules)) {
let outbound_tag = as_string(object_or_empty(rule).outboundTag || "");
if (supported_tags[outbound_tag])
return outbound_tag;
}
if (supported_tags["proxy"])
return "proxy";
for (let i = 0; i < length(source_outbounds); i++) {
let outbound = source_outbounds[i];
if (type(outbound) != "object" || !xray_protocol_supported(outbound.protocol))
continue;
let tag = xray_source_tag(outbound, lc(as_string(outbound.protocol || "server")) + "-" + (i + 1));
if (xray_outbound_detour(outbound) == "")
return tag;
}
for (let tag in keys(supported_tags))
return tag;
return "";
}
function xray_balancer_selector_matches(selector, tag) {
selector = as_string(selector);
tag = as_string(tag);
if (selector == "" || tag == "")
return false;
return tag == selector || starts_with(tag, selector + "-") || starts_with(tag, selector);
}
function xray_urltest_option_string(value) {
value = as_string(value);
return value != "" ? value : "";
}
function xray_config_urltest_options(config) {
let observatory = object_or_empty(config.observatory);
let burst_observatory = object_or_empty(config.burstObservatory || config.burst_observatory);
let ping_config = object_or_empty(burst_observatory.pingConfig || burst_observatory.ping_config);
let result = {};
let url = xray_first_string([
ping_config.destination,
ping_config.url,
observatory.probeURL,
observatory.probeUrl,
observatory.probe_url
]);
if (url != "")
result.url = url;
let interval = xray_first_string([
ping_config.interval,
observatory.probeInterval,
observatory.probe_interval
]);
if (interval != "")
result.interval = interval;
return result;
}
function xray_apply_urltest_options(group, options) {
options = object_or_empty(options);
for (let key in [ "url", "interval" ]) {
let value = xray_urltest_option_string(options[key]);
if (value != "")
group[key] = value;
}
}
function xray_balancer_plans(config, source_outbounds) {
let supported_tags = xray_supported_source_tags(source_outbounds);
let result = [];
let urltest_options = xray_config_urltest_options(config);
for (let balancer in array_or_empty(object_or_empty(config.routing).balancers)) {
let selectors = array_or_empty(object_or_empty(balancer).selector);
let selected = {};
let tags = [];
for (let tag in keys(supported_tags)) {
for (let selector in selectors) {
if (xray_balancer_selector_matches(selector, tag) && !selected[tag]) {
selected[tag] = true;
push(tags, tag);
break;
}
}
}
if (length(tags) > 0) {
push(result, {
tag: as_string(object_or_empty(balancer).tag || ""),
source_tags: tags,
urltest_options
});
}
}
return result;
}
function xray_balancer_group_base(plan, display_name, config_index, config_count, plan_index, plan_count) {
let balancer_tag = as_string(object_or_empty(plan).tag || "");
display_name = as_string(display_name);
if (plan_count == 1 && display_name != "")
return display_name;
if (balancer_tag != "")
return balancer_tag;
if (display_name != "")
return display_name + "-" + (plan_index + 1);
return xray_tag_base("urltest", config_index, config_count);
}
function xray_plan_config_outbounds(config, config_index, config_count, taken) {
let source_outbounds = array_or_empty(config.outbounds);
let planned = [];
let tag_map = {};
for (let i = 0; i < length(source_outbounds); i++) {
let outbound = source_outbounds[i];
if (type(outbound) != "object" || !xray_protocol_supported(outbound.protocol))
continue;
let original_tag = xray_source_tag(outbound, lc(as_string(outbound.protocol || "server")) + "-" + (i + 1));
let tag = xray_unique_tag(xray_tag_base(original_tag, config_index, config_count), taken);
taken[tag] = true;
tag_map[original_tag] = tag;
push(planned, { outbound, original_tag, tag });
}
return { planned, tag_map };
}
function xray_retag_planned_outbound(planned, tag_map, source_tag, new_base, config_index, config_count, taken) {
source_tag = as_string(source_tag);
new_base = as_string(new_base);
if (source_tag == "" || new_base == "")
return;
for (let item in planned) {
if (item.original_tag != source_tag)
continue;
let old_tag = as_string(item.tag || "");
let new_tag = xray_unique_tag(new_base, taken);
if (new_tag == old_tag)
return;
if (old_tag != "")
delete taken[old_tag];
taken[new_tag] = true;
item.tag = new_tag;
tag_map[source_tag] = new_tag;
return;
}
}
function xray_add_converted_outbound(result, item, tag_map, visible, display_name) {
let converted = convert_xray_outbound(item.outbound, item.tag);
if (!converted)
return;
if (visible && display_name != "")
converted.remark = display_name;
if (!visible)
converted.__tachyon_hidden = true;
let detour = xray_outbound_detour(item.outbound);
if (detour != "" && tag_map[detour])
converted.detour = tag_map[detour];
push(result, converted);
}
function xray_config_outbounds(config, config_index, config_count, taken) {
let source_outbounds = array_or_empty(config.outbounds);
let plan = xray_plan_config_outbounds(config, config_index, config_count, taken);
let planned = array_or_empty(plan.planned);
let tag_map = object_or_empty(plan.tag_map);
let display_name = xray_config_display_name(config, "");
let balancer_plans = xray_balancer_plans(config, source_outbounds);
let visible_source_tags = {};
let dependency_tags = {};
let result = [];
if (length(balancer_plans) > 0) {
for (let plan_index = 0; plan_index < length(balancer_plans); plan_index++) {
let plan = balancer_plans[plan_index];
let urltest_outbounds = [];
for (let source_tag in array_or_empty(plan.source_tags)) {
if (tag_map[source_tag]) {
visible_source_tags[source_tag] = false;
push(urltest_outbounds, tag_map[source_tag]);
}
}
if (length(urltest_outbounds) > 0) {
let group_base = xray_balancer_group_base(plan, display_name, config_index, config_count, plan_index, length(balancer_plans));
let group_tag = xray_unique_tag(group_base, taken);
taken[group_tag] = true;
let group = {
type: "urltest",
tag: group_tag,
outbounds: urltest_outbounds,
remark: group_base != "" ? group_base : group_tag,
__tachyon_allow_group: true
};
xray_apply_urltest_options(group, plan.urltest_options);
push(result, group);
}
}
} else {
let primary_tag = xray_primary_source_tag(config, source_outbounds);
if (primary_tag != "") {
xray_retag_planned_outbound(planned, tag_map, primary_tag, display_name, config_index, config_count, taken);
visible_source_tags[primary_tag] = true;
}
}
for (let item in planned) {
let detour = xray_outbound_detour(item.outbound);
if (detour != "" && tag_map[detour])
dependency_tags[detour] = true;
}
for (let item in planned) {
let visible = visible_source_tags[item.original_tag] === true;
let hidden = !visible;
if (visible || dependency_tags[item.original_tag] || visible_source_tags[item.original_tag] === false) {
let item_display_name = visible && display_name != ""
? display_name
: xray_display_name(item.original_tag);
xray_add_converted_outbound(result, item, tag_map, !hidden, item_display_name);
}
}
return result;
}
function is_xray_config(value) {
if (type(value) != "object" || type(value.outbounds) != "array")
return false;
for (let outbound in value.outbounds) {
if (type(outbound) == "object" && type(outbound.protocol) == "string")
return true;
}
return false;
}
function normalize_xray_configs(configs) {
configs = array_or_empty(configs);
let taken = {};
let outbounds = [];
for (let i = 0; i < length(configs); i++) {
let config = configs[i];
if (!is_xray_config(config))
continue;
for (let outbound in xray_config_outbounds(config, i, length(configs), taken))
push(outbounds, outbound);
}
return outbounds;
}
function normalize_sing_box_xhttp_transport(outbound) {
if (type(outbound) != "object" || type(outbound.transport) != "object")
return outbound;
if (outbound.transport.type != "xhttp")
return outbound;
outbound.transport.x_padding_bytes = xhttp_positive_range_or_default(outbound.transport.x_padding_bytes, "100-1000");
let sc_max_each_post_bytes = xhttp_present_positive_range_or_default(outbound.transport.sc_max_each_post_bytes, "1000000");
if (sc_max_each_post_bytes != null)
outbound.transport.sc_max_each_post_bytes = sc_max_each_post_bytes;
let sc_min_posts_interval_ms = xhttp_present_positive_range_or_default(outbound.transport.sc_min_posts_interval_ms, "30");
if (sc_min_posts_interval_ms != null)
outbound.transport.sc_min_posts_interval_ms = sc_min_posts_interval_ms;
let uplink_chunk_size = xhttp_present_positive_range_or_default(outbound.transport.uplink_chunk_size, "2048-3072");
if (uplink_chunk_size != null)
outbound.transport.uplink_chunk_size = uplink_chunk_size;
let sc_stream_up_server_secs = xhttp_present_positive_range_or_default(outbound.transport.sc_stream_up_server_secs, "0");
if (sc_stream_up_server_secs != null)
outbound.transport.sc_stream_up_server_secs = sc_stream_up_server_secs;
return outbound;
}
function normalize_sing_box_hysteria2_outbound(outbound) {
if (type(outbound) == "object" &&
lc(as_string(outbound.type || "")) == "hysteria2" &&
type(outbound.tls) == "object" &&
outbound.tls.utls != null) {
delete outbound.tls.utls;
}
return outbound;
}
function normalize_sing_box_tuic_outbound(outbound) {
if (type(outbound) == "object" &&
lc(as_string(outbound.type || "")) == "tuic" &&
type(outbound.tls) == "object" &&
outbound.tls.utls != null) {
delete outbound.tls.utls;
}
return outbound;
}
function sing_box_urltest_group(outbound) {
return type(outbound) == "object" && lc(as_string(outbound.type || "")) == "urltest";
}
function sing_box_outbound_tag(outbound) {
return type(outbound) == "object" ? as_string(outbound.tag || "") : "";
}
function normalize_sing_box_json_outbounds(candidates) {
let outbounds = [];
let urltest_refs = {};
let detour_refs = {};
for (let outbound in candidates) {
if (type(outbound) != "object")
continue;
outbound = normalize_sing_box_hysteria2_outbound(outbound);
outbound = normalize_sing_box_tuic_outbound(outbound);
push(outbounds, normalize_sing_box_xhttp_transport(outbound));
}
for (let outbound in outbounds) {
if (sing_box_urltest_group(outbound)) {
for (let tag_name in array_or_empty(outbound.outbounds)) {
tag_name = as_string(tag_name);
if (tag_name != "")
urltest_refs[tag_name] = true;
}
}
let detour = as_string(outbound.detour || "");
if (detour != "")
detour_refs[detour] = true;
}
for (let outbound in outbounds) {
let tag_name = sing_box_outbound_tag(outbound);
if (sing_box_urltest_group(outbound)) {
outbound.__tachyon_allow_group = true;
continue;
}
if (tag_name != "" && (urltest_refs[tag_name] || detour_refs[tag_name]))
outbound.__tachyon_hidden = true;
}
return outbounds;
}
function normalize_sing_box_json_value(value, output_file) {
let candidates = [];
let outbounds = [];
if (is_xray_config(value))
outbounds = normalize_xray_configs([value]);
else if (type(value) == "array" && length(value) > 0 && is_xray_config(value[0]))
outbounds = normalize_xray_configs(value);
if (length(outbounds) > 0) {
return write_json_file(output_file, {
version: 1,
format: "sing-box-json",
outbounds: outbounds
});
}
if (type(value) == "object" && type(value.outbounds) == "array")
candidates = value.outbounds;
else if (type(value) == "array")
candidates = value;
else if (type(value) == "object" && type(value.type) == "string")
candidates = [value];
outbounds = normalize_sing_box_json_outbounds(candidates);
if (length(outbounds) == 0) {
fs.unlink(output_file);
return false;
}
return write_json_file(output_file, {
version: 1,
format: "sing-box-json",
outbounds: outbounds
});
}
function temp_path(prefix) {
let stamp = clock();
return sprintf("/tmp/%s.%d.%d", prefix, stamp[0], stamp[1]);
}
function shell_quote(value) {
return "'" + replace(as_string(value), /'/g, "'\\''") + "'";
}
function gzip_decode_file(input_file, output_file) {
if (as_string(input_file) == "" || as_string(output_file) == "" || !fs.stat(input_file))
return false;
for (let command in [ "gzip -dc", "gunzip -c", "zcat" ]) {
let pipe = fs.popen(command + " " + shell_quote(input_file) + " 2>/dev/null", "r");
if (!pipe)
continue;
let data = pipe.read("all");
let status = pipe.close();
if (status == 0 && data != null && data != "") {
if (fs.writefile(output_file, data))
return true;
fs.unlink(output_file);
return false;
}
fs.unlink(output_file);
}
return false;
}
function try_decode_gzip_content_file(input_file) {
let tmp = temp_path("tachyon-subscription-gzip");
if (!gzip_decode_file(input_file, tmp)) {
fs.unlink(tmp);
return false;
}
if (!fs.rename(tmp, input_file)) {
fs.unlink(tmp);
return false;
}
return true;
}
function normalized_skipped_message(path) {
let value = read_json_file(path);
if (type(value) != "object")
return "";
let skipped = as_string(value.skipped != null ? value.skipped : "0");
if (match(skipped, /^[0-9]+$/) == null || int(skipped) <= 0)
return "";
if (as_string(value.format != null ? value.format : "") == "clash-yaml")
return "Skipped " + skipped + " invalid or unsupported Clash proxy entries";
return "Skipped " + skipped + " invalid or unsupported subscription links";
}
function extract_ui_metadata_file(headers_file, body_file, output_file) {
let tmp = temp_path("tachyon-subscription-metadata");
let raw = metadata_body_store_with_base64_fallback(body_file).values;
let headers = metadata_headers_store(headers_file).values;
for (let key, value in headers)
raw[key] = value;
let result = metadata_ui_object(raw);
if (length(keys(result)) <= 1) {
fs.unlink(output_file);
return false;
}
if (!fs.writefile(tmp, sprintf("%J\n", result))) {
fs.unlink(tmp);
return false;
}
if (!fs.rename(tmp, output_file)) {
fs.unlink(tmp);
return false;
}
return true;
}
function normalize_content_data(data, output_file, depth) {
data = as_string(data);
if (index(data, "\r") >= 0)
data = replace(data, /\r/g, "");
data = strip_metadata_preamble_data(data);
let first = first_non_ws_char(data);
if (first == "{" || first == "[") {
let decoded_json = json_decode_text(data);
if (decoded_json != null)
return normalize_sing_box_json_value(decoded_json, output_file);
}
if (index(data, "proxies:") >= 0 && content_is_clash_yaml(data)) {
let tmp = temp_path("tachyon-subscription-clash");
if (!fs.writefile(tmp, data))
return false;
let ok = normalize_clash_yaml(tmp, output_file);
fs.unlink(tmp);
return ok;
}
if (index(data, "://") >= 0) {
if (normalize_uri_list_data(data, output_file))
return true;
}
if (depth < 1) {
let compact = replace(data, /[\r\n\t ]/g, "");
let decoded = base64_decode(compact);
if (decoded != null && decoded != "")
return normalize_content_data(decoded, output_file, depth + 1);
}
return false;
}
function normalize_content_file(input_file, output_file) {
if (file_looks_like_uri_list(input_file))
return normalize_uri_list_file(input_file, output_file, true);
let data = read_file(input_file);
return data == null ? false : normalize_content_data(data, output_file, 0);
}
function normalize_content_validated(input_file, output_file) {
let normalize_input = input_file;
let decoded_tmp = temp_path("tachyon-subscription-gzip");
let decoded = gzip_decode_file(input_file, decoded_tmp);
if (decoded)
normalize_input = decoded_tmp;
else
fs.unlink(decoded_tmp);
let ok = normalize_content_file(normalize_input, output_file);
if (decoded)
fs.unlink(decoded_tmp);
if (!ok)
return false;
return validate_subscription(output_file);
}
function normalized_skipped_warning(path) {
let message = normalized_skipped_message(path);
if (message != "")
print(message, "\n");
return true;
}
function subscription_source_entry_result(valid, url, user_agent, error) {
return {
valid,
url: as_string(url),
user_agent: as_string(user_agent),
error: as_string(error)
};
}
function write_subscription_source_entry_tsv_result(result) {
result = type(result) == "object" ? result : subscription_source_entry_result(false, "", "", "Subscription URL cannot be empty");
if (!result.valid) {
print(result.error, "\n");
return false;
}
print(result.url, "\t", result.user_agent, "\n");
return true;
}
function parse_subscription_source_entry(entry) {
entry = trim(entry);
if (entry == "")
return subscription_source_entry_result(false, "", "", "Subscription URL cannot be empty");
if (index(entry, "|") >= 0)
return subscription_source_entry_result(false, "", "", "Configure User-Agent in the subscription item settings");
if (!(substr(entry, 0, 7) == "http://" || substr(entry, 0, 8) == "https://"))
return subscription_source_entry_result(false, "", "", "Subscription URL must start with http:// or https://");
return subscription_source_entry_result(true, entry, "", "");
}
function parse_subscription_source_entry_tsv(entry) {
return write_subscription_source_entry_tsv_result(parse_subscription_source_entry(entry));
}
function module_exports() {
return {
parse_subscription_source_entry,
validate_subscription,
normalize_content_validated,
normalized_skipped_message,
try_decode_gzip_content_file,
extract_ui_metadata_file,
runtime_outbounds_equal
};
}
if (sourcepath(1) != null && sourcepath(1) != "")
return module_exports();
let mode = ARGV[0];
let ok = false;
if (mode == "validate-subscription")
ok = validate_subscription(ARGV[1]);
else if (mode == "url-fragment")
ok = subscription_url_fragment(ARGV[1]);
else if (mode == "share-link-outbound")
ok = share_link_outbound(ARGV[1], ARGV[2]);
else if (mode == "normalize-uri-list")
ok = normalize_uri_list(ARGV[1], ARGV[2]);
else if (mode == "normalize-clash-yaml")
ok = normalize_clash_yaml(ARGV[1], ARGV[2]);
else if (mode == "normalize-content")
ok = normalize_content_file(ARGV[1], ARGV[2]);
else if (mode == "normalize-content-validated")
ok = normalize_content_validated(ARGV[1], ARGV[2]);
else if (mode == "try-decode-gzip-content")
ok = try_decode_gzip_content_file(ARGV[1]);
else if (mode == "runtime-outbounds-equal")
ok = runtime_outbounds_equal(ARGV[1], ARGV[2]);
else if (mode == "normalized-skipped-warning")
ok = normalized_skipped_warning(ARGV[1]);
else if (mode == "metadata-headers-json")
ok = metadata_headers_json(ARGV[1]);
else if (mode == "metadata-body-json")
ok = metadata_body_json(ARGV[1]);
else if (mode == "metadata-ui-json")
ok = metadata_ui_json(ARGV[1]);
else if (mode == "metadata-extract-ui-json")
ok = metadata_extract_ui_json(ARGV[1], ARGV[2]);
else if (mode == "metadata-extract-ui-file")
ok = extract_ui_metadata_file(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "parse-source-entry-tsv")
ok = parse_subscription_source_entry_tsv(ARGV[1]);
else if (ARGV[0] && ARGV[1])
ok = normalize_uri_list(ARGV[0], ARGV[1]);
else {
warn("Usage: subscription/parser.uc validate-subscription <normalized-json>\n");
warn(" subscription/parser.uc url-fragment <url>\n");
warn(" subscription/parser.uc share-link-outbound <url> <tag>\n");
warn(" subscription/parser.uc normalize-uri-list <input> <output>\n");
warn(" subscription/parser.uc normalize-clash-yaml <input> <output>\n");
warn(" subscription/parser.uc normalize-content <input> <output>\n");
warn(" subscription/parser.uc normalize-content-validated <input> <output>\n");
warn(" subscription/parser.uc try-decode-gzip-content <input>\n");
warn(" subscription/parser.uc runtime-outbounds-equal <left> <right>\n");
warn(" subscription/parser.uc normalized-skipped-warning <normalized-json>\n");
warn(" subscription/parser.uc metadata-headers-json <input>\n");
warn(" subscription/parser.uc metadata-body-json <input>\n");
warn(" subscription/parser.uc metadata-ui-json <raw-json>\n");
warn(" subscription/parser.uc metadata-extract-ui-json <headers> <body>\n");
warn(" subscription/parser.uc metadata-extract-ui-file <headers> <body> <output>\n");
warn(" subscription/parser.uc parse-source-entry-tsv <entry>\n");
exit(2);
}
if (!ok)
exit(1);