#!/usr/bin/env ucode let fs = require("fs"); let common = require("core.common"); let crypt4 = require("subscription.crypt4"); let as_string = common.as_string; let object_or_empty = common.object_or_empty; let array_or_empty = common.array_or_empty; let write_json = common.write_json; 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://") || starts_with(line, "http://") || starts_with(line, "https://") || crypt4.is_crypt4(line); } 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_dummy_server(server, port) { server = trim(lc(as_string(server))); if (server == "" || server == "0.0.0.0" || server == "::" || substr(server, 0, 8) == "0.0.0.0:") return true; if (port != null && int(port) == 0) return true; return false; } 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", "sessionPlacement", "session_placement", "sessionKey", "session_key", "seqPlacement", "seq_placement", "seqKey", "seq_key", "uplinkDataPlacement", "uplink_data_placement", "uplinkDataKey", "uplink_data_key", "uplinkChunkSize", "uplink_chunk_size", "uplinkHttpMethod", "uplink_http_method", "xPaddingObfsMode", "x_padding_obfs_mode", "xPaddingPlacement", "x_padding_placement", "xPaddingKey", "x_padding_key", "xPaddingHeader", "x_padding_header", "xPaddingMethod", "x_padding_method", "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 xhttp_optional_string(object, key, value) { if (!xhttp_value_present(value)) return; value = trim(as_string(value)); if (value != "") object[key] = value; } function xhttp_optional_enum(object, key, value, allowed) { if (!xhttp_value_present(value)) return; let normalized = lc(trim(as_string(value))); for (let candidate in allowed) { if (normalized == lc(as_string(candidate))) { object[key] = as_string(candidate); return; } } } function xhttp_apply_v2_settings(result, primary, extra) { // XHTTP v2 keys (session/seq placement, uplink-data placement, X-Padding // obfuscation). Every field is opt-in per the sing-box-lx contract; the // v1 wire shape stays byte-identical when none are present. xhttp_optional_enum(result, "session_placement", xhttp_setting_value(primary, extra, "sessionPlacement", "session_placement"), [ "path", "query", "header", "cookie" ]); xhttp_optional_string(result, "session_key", xhttp_setting_value(primary, extra, "sessionKey", "session_key")); xhttp_optional_enum(result, "seq_placement", xhttp_setting_value(primary, extra, "seqPlacement", "seq_placement"), [ "path", "query", "header", "cookie" ]); xhttp_optional_string(result, "seq_key", xhttp_setting_value(primary, extra, "seqKey", "seq_key")); xhttp_optional_enum(result, "uplink_data_placement", xhttp_setting_value(primary, extra, "uplinkDataPlacement", "uplink_data_placement"), [ "body", "auto", "header", "cookie" ]); xhttp_optional_string(result, "uplink_data_key", xhttp_setting_value(primary, extra, "uplinkDataKey", "uplink_data_key")); xhttp_optional_positive_range(result, "uplink_chunk_size", xhttp_setting_value(primary, extra, "uplinkChunkSize", "uplink_chunk_size")); xhttp_optional_enum(result, "uplink_http_method", xhttp_setting_value(primary, extra, "uplinkHttpMethod", "uplink_http_method"), [ "POST", "GET" ]); xhttp_optional_bool(result, "x_padding_obfs_mode", xhttp_setting_value(primary, extra, "xPaddingObfsMode", "x_padding_obfs_mode")); xhttp_optional_enum(result, "x_padding_placement", xhttp_setting_value(primary, extra, "xPaddingPlacement", "x_padding_placement"), [ "cookie", "header", "query", "queryInHeader" ]); xhttp_optional_string(result, "x_padding_key", xhttp_setting_value(primary, extra, "xPaddingKey", "x_padding_key")); xhttp_optional_string(result, "x_padding_header", xhttp_setting_value(primary, extra, "xPaddingHeader", "x_padding_header")); xhttp_optional_enum(result, "x_padding_method", xhttp_setting_value(primary, extra, "xPaddingMethod", "x_padding_method"), [ "repeat-x", "tokenish" ]); } 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 outbound_is_unsupported_stub(outbound) { if (type(outbound) != "object") return false; let tag = lc(as_string(outbound.tag || outbound.name || outbound.remark || "")); let server = lc(trim(as_string(outbound.server || ""))); if (index(tag, "не поддерживается") >= 0 || index(tag, "неподдерживаем") >= 0 || index(tag, "клиент не поддерживается") >= 0 || index(tag, "приложение не поддерживается") >= 0 || index(tag, "unsupported") >= 0 || index(tag, "not supported") >= 0 || index(tag, "update app") >= 0 || index(tag, "update client") >= 0 || index(tag, "обновите приложение") >= 0 || index(tag, "обновите клиент") >= 0 || index(tag, "скачайте happ") >= 0 || index(tag, "скачать happ") >= 0 || index(tag, "используйте happ") >= 0 || index(tag, "use happ") >= 0 || index(tag, "download happ") >= 0 || index(tag, "скачайте v2box") >= 0 || index(tag, "используйте v2box") >= 0 || index(tag, "скачайте v2rayn") >= 0 || index(tag, "используйте v2rayn") >= 0) return true; let is_dummy_server = ( server == "" || server == "127.0.0.1" || server == "0.0.0.0" || server == "localhost" || server == "::1" || server == "0.0.0.1" ); if (is_dummy_server && ( index(tag, "⚠️") >= 0 || index(tag, "❗") >= 0 || index(tag, "❌") >= 0 || index(tag, "warning") >= 0 || index(tag, "error") >= 0 || index(tag, "notice") >= 0 || index(tag, "attention") >= 0 || index(tag, "внимание") >= 0 || index(tag, "ошибка") >= 0 )) return true; return false; } function validate_subscription(path) { let value = read_json_file(path); if (type(value) != "object" || type(value.outbounds) != "array" || length(value.outbounds) == 0) return false; let usable_count = 0; for (let outbound in value.outbounds) { if (outbound_is_unsupported_stub(outbound)) continue; 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; usable_count++; } return usable_count > 0; } function write_json_file(path, value) { return fs.writefile(path, 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; let result = []; for (let outbound in subscription.outbounds) push(result, sprintf("%J", canonical_runtime_value(outbound))); sort(result, function(a, b) { return a < b ? -1 : (a > b ? 1 : 0); }); return result; } 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 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 || query.service_name || ""; 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; xhttp_apply_v2_settings(result, query, extra_settings); 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 == "" || is_dummy_server(url.host, url.port) || !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 == "" || is_dummy_server(url.host, url.port) || !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 == "" || is_dummy_server(url.host, url.port) || !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 process_http(raw, url) { let port = url.port; if (port == null || port == "") port = url.scheme == "https" ? 443 : 80; if (url.host == "" || is_dummy_server(url.host, port) || !valid_port(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: "http", tag: url.fragment != "" ? url.fragment : (url.host + ":" + port), share_link: raw, server: url.host, server_port: int(port) }; if (username != "") outbound.username = username; if (password != "") outbound.password = password; if (url.scheme == "https") { 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; outbound.tls = tls; } 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] == "" || is_dummy_server(host_port[0], host_port[1]) || !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 == "" || is_dummy_server(url.host, port_value) || (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 == "" || is_dummy_server(url.host, url.port) || 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 == "" || is_dummy_server(server, port) || !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); if (url.scheme == "http" || url.scheme == "https") return process_http(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 == "" || is_dummy_server(server, port) || !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; } if (proxy_type == "http" || proxy_type == "https") { let outbound = { type: "http", tag: name, server: server, server_port: port }; if (as_string(record.username) != "") outbound.username = as_string(record.username); if (as_string(record.password) != "") outbound.password = as_string(record.password); if (proxy_type == "https" || is_true(record.tls)) outbound.tls = { enabled: true }; 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) { if (outbound_is_unsupported_stub(outbound)) { skipped++; continue; } 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, "#")) { if (crypt4.is_crypt4(line)) { let decrypted = crypt4.decrypt(line); if (decrypted != null && decrypted != "") { for (let dec_line in split(decrypted, "\n")) { dec_line = trim(dec_line); if (dec_line != "" && !starts_with(dec_line, "#")) { let outbound = parse_share_link(dec_line); if (outbound) { if (outbound_is_unsupported_stub(outbound)) { skipped++; continue; } if (added > 0) output.write(","); output.write(sprintf("%J", outbound)); added++; } else { skipped++; } } } continue; } } let outbound = parse_share_link(line); if (outbound) { if (outbound_is_unsupported_stub(outbound)) { skipped++; continue; } 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 hex_val(ch) { let code = ord(ch); if (code >= 48 && code <= 57) return code - 48; if (code >= 65 && code <= 70) return code - 55; if (code >= 97 && code <= 102) return code - 87; return 0; } function hex_to_int(str) { if (length(str) < 2) return 0; return (hex_val(substr(str, 0, 1)) << 4) | hex_val(substr(str, 1, 1)); } function decode_url_component(str) { str = as_string(str); str = replace(str, /\+/g, " "); return replace(str, /%([0-9A-Fa-f]{2})/g, function(match, hex) { return chr(hex_to_int(hex)); }); } function decode_rfc2047(val) { val = as_string(val); let m = match(val, /^=\?([^?]+)\?([BbQq])\?([^?]+)\?=$/); if (m) { let encoding = lc(m[2]); let data = m[3]; if (encoding == "b") { let decoded = base64_decode(data); if (decoded != null) return decoded; } else if (encoding == "q") { let qdecoded = replace(data, /_/g, " "); qdecoded = replace(qdecoded, /=([0-9A-Fa-f]{2})/g, function(full, hex) { return chr(hex_to_int(hex)); }); return qdecoded; } } return val; } function metadata_clean_text(value, max, decode_base64) { value = as_string(value); if (starts_with(value, "=?")) value = decode_rfc2047(value); if (decode_base64 && lc(substr(value, 0, 7)) == "base64:") { let raw_val = substr(value, 7); let decoded = base64_decode(raw_val); if (decoded != null && length(decoded) > 0 && match(decoded, /[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/) == null) { value = decoded; } else { value = raw_val; } } if (index(value, "%") >= 0) { value = decode_url_component(value); } let cleaned = trim(replace(replace(value, /[\x01-\x1f\x7f]+/g, " "), / +/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), /[\x01-\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 pos_ext = index(value, "filename*="); if (pos_ext >= 0) { let rest = substr(value, pos_ext + 10); let semicolon = index(rest, ";"); let ext_val = semicolon >= 0 ? substr(rest, 0, semicolon) : rest; ext_val = trim(ext_val); let quote = index(ext_val, "''"); if (quote >= 0) { filename = decode_url_component(substr(ext_val, quote + 2)); } else { filename = decode_url_component(ext_val); } } if (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) { 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); } } } if (filename == null) return null; 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, "#")) { if (crypt4.is_crypt4(line)) { let decrypted = crypt4.decrypt(line); if (decrypted != null && decrypted != "") { for (let dec_line in split(decrypted, "\n")) { dec_line = trim(dec_line); if (dec_line != "" && !starts_with(dec_line, "#")) { let outbound = parse_share_link(dec_line); if (outbound) { if (outbound_is_unsupported_stub(outbound)) { skipped++; continue; } if (added > 0) output.write(","); output.write(sprintf("%J", outbound)); added++; } else { skipped++; } } } continue; } } let outbound = parse_share_link(line); if (outbound) { if (outbound_is_unsupported_stub(outbound)) { skipped++; continue; } 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 (crypt4.is_crypt4(line)) { input.close(); return false; } 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; xhttp_apply_v2_settings(result, settings, settings); 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 == "" || is_dummy_server(server, port) || 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 == "" || is_dummy_server(server, port) || 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 == "" || is_dummy_server(server, port) || 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" || outbound_is_unsupported_stub(outbound)) 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 status = system(command + " " + shell_quote(input_file) + " > " + shell_quote(output_file) + " 2>/dev/null"); if (status == 0) { let stat = fs.stat(output_file); if (stat && int(stat.size) > 0) return true; } 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); if (depth < 2 && crypt4.is_crypt4(data)) { let decrypted = crypt4.decrypt(data); if (decrypted != null && decrypted != "") return normalize_content_data(decrypted, output_file, depth + 1); } 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://" || crypt4.is_crypt4(entry))) 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, outbound_is_unsupported_stub }; } if ((sourcepath(1) != null && sourcepath(1) != "") || ARGV[0] == null) 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 \n"); warn(" subscription/parser.uc url-fragment \n"); warn(" subscription/parser.uc share-link-outbound \n"); warn(" subscription/parser.uc normalize-uri-list \n"); warn(" subscription/parser.uc normalize-clash-yaml \n"); warn(" subscription/parser.uc normalize-content \n"); warn(" subscription/parser.uc normalize-content-validated \n"); warn(" subscription/parser.uc try-decode-gzip-content \n"); warn(" subscription/parser.uc runtime-outbounds-equal \n"); warn(" subscription/parser.uc normalized-skipped-warning \n"); warn(" subscription/parser.uc metadata-headers-json \n"); warn(" subscription/parser.uc metadata-body-json \n"); warn(" subscription/parser.uc metadata-ui-json \n"); warn(" subscription/parser.uc metadata-extract-ui-json \n"); warn(" subscription/parser.uc metadata-extract-ui-file \n"); warn(" subscription/parser.uc parse-source-entry-tsv \n"); exit(2); } if (!ok) exit(1);