488 lines
16 KiB
Ucode
488 lines
16 KiB
Ucode
#!/usr/bin/env ucode
|
|
|
|
let fs = require("fs");
|
|
let common = require("core.common");
|
|
|
|
let as_string = common.as_string;
|
|
let array_or_empty = common.array_or_empty;
|
|
let shell_quote = common.shell_quote;
|
|
|
|
function starts_with(value, prefix) {
|
|
value = as_string(value);
|
|
prefix = as_string(prefix);
|
|
return substr(value, 0, length(prefix)) == prefix;
|
|
}
|
|
|
|
function uri_encode(value) {
|
|
value = as_string(value);
|
|
let result = "";
|
|
let len = length(value);
|
|
for (let i = 0; i < len; i++) {
|
|
let code = ord(value, i);
|
|
if ((code >= 48 && code <= 57) ||
|
|
(code >= 65 && code <= 90) ||
|
|
(code >= 97 && code <= 122) ||
|
|
code == 45 || code == 95 || code == 46 || code == 126)
|
|
result += chr(code);
|
|
else
|
|
result += sprintf("%%%02X", code);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function base64_encode(value) {
|
|
return replace(b64enc(as_string(value)), /=+$/, "");
|
|
}
|
|
|
|
function host_port(server, port) {
|
|
server = as_string(server);
|
|
if (index(server, ":") >= 0 && !starts_with(server, "["))
|
|
server = "[" + server + "]";
|
|
return server + ":" + as_string(port);
|
|
}
|
|
|
|
function hysteria2_server_port_entry(value) {
|
|
value = as_string(value);
|
|
let colon = index(value, ":");
|
|
if (colon < 0)
|
|
return value;
|
|
|
|
let start = substr(value, 0, colon);
|
|
let end = substr(value, colon + 1);
|
|
if (start == "" || end == "")
|
|
return "";
|
|
|
|
return start == end ? start : (start + "-" + end);
|
|
}
|
|
|
|
function hysteria2_server_ports_uri(outbound) {
|
|
let server_ports = array_or_empty(outbound.server_ports);
|
|
if (length(server_ports) == 0)
|
|
return "";
|
|
|
|
let result = [];
|
|
for (let item in server_ports) {
|
|
let port = hysteria2_server_port_entry(item);
|
|
if (port != "")
|
|
push(result, port);
|
|
}
|
|
|
|
return join(",", result);
|
|
}
|
|
|
|
function add_query(params, key, value) {
|
|
value = as_string(value);
|
|
if (value != "")
|
|
push(params, uri_encode(key) + "=" + uri_encode(value));
|
|
}
|
|
|
|
function add_xhttp_extra_query(params, transport) {
|
|
let extra = {};
|
|
for (let item 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"]
|
|
]) {
|
|
if (transport[item[1]] != null)
|
|
extra[item[0]] = transport[item[1]];
|
|
}
|
|
|
|
if (type(transport.xmux) == "object") {
|
|
let xmux = {};
|
|
for (let item in [
|
|
["maxConcurrency", "max_concurrency"],
|
|
["maxConnections", "max_connections"],
|
|
["cMaxReuseTimes", "c_max_reuse_times"],
|
|
["hMaxRequestTimes", "h_max_request_times"],
|
|
["hMaxReusableSecs", "h_max_reusable_secs"],
|
|
["hKeepAlivePeriod", "h_keep_alive_period"]
|
|
]) {
|
|
if (transport.xmux[item[1]] != null)
|
|
xmux[item[0]] = transport.xmux[item[1]];
|
|
}
|
|
if (length(keys(xmux)) > 0)
|
|
extra.xmux = xmux;
|
|
}
|
|
|
|
if (length(keys(extra)) > 0)
|
|
add_query(params, "extra", sprintf("%J", extra));
|
|
}
|
|
|
|
function add_tls_query(params, outbound, trojan_default_tls) {
|
|
let tls = type(outbound.tls) == "object" ? outbound.tls : null;
|
|
if (!tls || tls.enabled === false) {
|
|
if (trojan_default_tls)
|
|
add_query(params, "security", "tls");
|
|
return;
|
|
}
|
|
|
|
let reality = type(tls.reality) == "object" ? tls.reality : null;
|
|
if (reality && reality.enabled !== false) {
|
|
add_query(params, "security", "reality");
|
|
add_query(params, "pbk", reality.public_key);
|
|
add_query(params, "sid", reality.short_id);
|
|
}
|
|
else {
|
|
add_query(params, "security", "tls");
|
|
}
|
|
|
|
add_query(params, "sni", tls.server_name);
|
|
if (tls.insecure === true)
|
|
add_query(params, "allowInsecure", "1");
|
|
if (type(tls.utls) == "object" && tls.utls.enabled !== false)
|
|
add_query(params, "fp", tls.utls.fingerprint);
|
|
if (type(tls.alpn) == "array" && length(tls.alpn) > 0)
|
|
add_query(params, "alpn", join(",", tls.alpn));
|
|
}
|
|
|
|
function add_transport_query(params, outbound) {
|
|
let transport = type(outbound.transport) == "object" ? outbound.transport : null;
|
|
if (!transport) {
|
|
add_query(params, "type", "tcp");
|
|
return;
|
|
}
|
|
|
|
let transport_type = as_string(transport.type);
|
|
add_query(params, "type", transport_type != "" ? transport_type : "tcp");
|
|
|
|
if (transport_type == "ws") {
|
|
add_query(params, "path", transport.path);
|
|
if (type(transport.headers) == "object")
|
|
add_query(params, "host", transport.headers.Host || transport.headers.host);
|
|
}
|
|
else if (transport_type == "grpc") {
|
|
add_query(params, "serviceName", transport.service_name);
|
|
}
|
|
else if (transport_type == "http") {
|
|
add_query(params, "path", transport.path);
|
|
if (type(transport.host) == "array" && length(transport.host) > 0)
|
|
add_query(params, "host", join(",", transport.host));
|
|
else
|
|
add_query(params, "host", transport.host);
|
|
}
|
|
else if (transport_type == "xhttp") {
|
|
add_query(params, "path", transport.path);
|
|
add_query(params, "host", transport.host);
|
|
add_query(params, "mode", transport.mode);
|
|
add_xhttp_extra_query(params, transport);
|
|
}
|
|
}
|
|
|
|
function query_string(params) {
|
|
return length(params) == 0 ? "" : "?" + join("&", params);
|
|
}
|
|
|
|
function fragment(outbound) {
|
|
let tag = as_string(outbound.tag);
|
|
return tag == "" ? "" : "#" + uri_encode(tag);
|
|
}
|
|
|
|
function serialize_vless(outbound) {
|
|
if (as_string(outbound.uuid) == "" || as_string(outbound.server) == "" || outbound.server_port == null)
|
|
return "";
|
|
let params = [];
|
|
add_tls_query(params, outbound, false);
|
|
add_transport_query(params, outbound);
|
|
let encryption = as_string(outbound.encryption);
|
|
if (encryption != "" && encryption != "none")
|
|
add_query(params, "encryption", encryption);
|
|
add_query(params, "flow", outbound.flow);
|
|
add_query(params, "packetEncoding", outbound.packet_encoding);
|
|
return "vless://" + uri_encode(outbound.uuid) + "@" +
|
|
host_port(outbound.server, outbound.server_port) + query_string(params) + fragment(outbound);
|
|
}
|
|
|
|
function serialize_trojan(outbound) {
|
|
if (as_string(outbound.password) == "" || as_string(outbound.server) == "" || outbound.server_port == null)
|
|
return "";
|
|
let params = [];
|
|
add_tls_query(params, outbound, true);
|
|
add_transport_query(params, outbound);
|
|
return "trojan://" + uri_encode(outbound.password) + "@" +
|
|
host_port(outbound.server, outbound.server_port) + query_string(params) + fragment(outbound);
|
|
}
|
|
|
|
function serialize_shadowsocks(outbound) {
|
|
if (as_string(outbound.method) == "" || as_string(outbound.password) == "" ||
|
|
as_string(outbound.server) == "" || outbound.server_port == null)
|
|
return "";
|
|
let userinfo = base64_encode(as_string(outbound.method) + ":" + as_string(outbound.password));
|
|
return userinfo == "" ? "" :
|
|
"ss://" + userinfo + "@" + host_port(outbound.server, outbound.server_port) + fragment(outbound);
|
|
}
|
|
|
|
function serialize_socks(outbound) {
|
|
if (as_string(outbound.server) == "" || outbound.server_port == null)
|
|
return "";
|
|
|
|
let scheme = "socks" + as_string(outbound.version || "5");
|
|
let auth = "";
|
|
if (as_string(outbound.username) != "") {
|
|
auth = uri_encode(outbound.username);
|
|
if (as_string(outbound.password) != "")
|
|
auth += ":" + uri_encode(outbound.password);
|
|
auth += "@";
|
|
}
|
|
|
|
return scheme + "://" + auth + host_port(outbound.server, outbound.server_port) + fragment(outbound);
|
|
}
|
|
|
|
function serialize_http(outbound) {
|
|
if (as_string(outbound.server) == "" || outbound.server_port == null)
|
|
return "";
|
|
|
|
let is_tls = type(outbound.tls) == "object" && (outbound.tls.enabled === true || outbound.tls.enabled == 1);
|
|
let scheme = is_tls ? "https" : "http";
|
|
let auth = "";
|
|
if (as_string(outbound.username) != "") {
|
|
auth = uri_encode(outbound.username);
|
|
if (as_string(outbound.password) != "")
|
|
auth += ":" + uri_encode(outbound.password);
|
|
auth += "@";
|
|
}
|
|
|
|
let params = [];
|
|
if (is_tls && type(outbound.tls) == "object") {
|
|
let tls = outbound.tls;
|
|
if (tls.server_name)
|
|
add_query(params, "sni", tls.server_name);
|
|
if (tls.insecure === true)
|
|
add_query(params, "insecure", "1");
|
|
}
|
|
|
|
return scheme + "://" + auth + host_port(outbound.server, outbound.server_port) + query_string(params) + fragment(outbound);
|
|
}
|
|
|
|
|
|
function serialize_hysteria2(outbound) {
|
|
let port = hysteria2_server_ports_uri(outbound);
|
|
if (port == "" && outbound.server_port != null)
|
|
port = as_string(outbound.server_port);
|
|
|
|
if (as_string(outbound.password) == "" || as_string(outbound.server) == "" || port == "")
|
|
return "";
|
|
|
|
let params = [];
|
|
let tls = type(outbound.tls) == "object" ? outbound.tls : null;
|
|
if (tls) {
|
|
add_query(params, "sni", tls.server_name);
|
|
if (tls.insecure === true)
|
|
add_query(params, "insecure", "1");
|
|
if (type(tls.alpn) == "array" && length(tls.alpn) > 0)
|
|
add_query(params, "alpn", join(",", tls.alpn));
|
|
}
|
|
if (type(outbound.obfs) == "object") {
|
|
add_query(params, "obfs", outbound.obfs.type);
|
|
add_query(params, "obfs-password", outbound.obfs.password);
|
|
}
|
|
|
|
return "hysteria2://" + uri_encode(outbound.password) + "@" +
|
|
host_port(outbound.server, port) + query_string(params) + fragment(outbound);
|
|
}
|
|
|
|
function serialize_tuic(outbound) {
|
|
let port = as_string(outbound.server_port || "");
|
|
if (as_string(outbound.uuid || "") == "" || as_string(outbound.server || "") == "" || port == "")
|
|
return "";
|
|
|
|
let userinfo = as_string(outbound.uuid);
|
|
if (as_string(outbound.password || "") != "")
|
|
userinfo += ":" + outbound.password;
|
|
|
|
let params = [];
|
|
let tls = type(outbound.tls) == "object" ? outbound.tls : null;
|
|
if (tls) {
|
|
add_query(params, "sni", tls.server_name);
|
|
if (tls.insecure === true)
|
|
add_query(params, "insecure", "1");
|
|
if (type(tls.alpn) == "array" && length(tls.alpn) > 0)
|
|
add_query(params, "alpn", join(",", tls.alpn));
|
|
}
|
|
let cc = as_string(outbound.congestion_control || "");
|
|
if (cc != "" && cc != "bbr")
|
|
add_query(params, "congestion_control", cc);
|
|
let udp_mode = as_string(outbound.udp_relay_mode || "");
|
|
if (udp_mode != "" && udp_mode != "native")
|
|
add_query(params, "udp_relay_mode", udp_mode);
|
|
if (outbound.zero_rtt_handshake === true)
|
|
add_query(params, "zero_rtt_handshake", "1");
|
|
let hb = as_string(outbound.heartbeat || "");
|
|
if (hb != "")
|
|
add_query(params, "heartbeat", hb);
|
|
|
|
return "tuic://" + uri_encode(userinfo) + "@" +
|
|
host_port(outbound.server, port) + query_string(params) + fragment(outbound);
|
|
}
|
|
|
|
function serialize_vmess(outbound) {
|
|
if (as_string(outbound.uuid) == "" || as_string(outbound.server) == "" || outbound.server_port == null)
|
|
return "";
|
|
|
|
let vmess = {
|
|
v: "2",
|
|
ps: as_string(outbound.tag),
|
|
add: as_string(outbound.server),
|
|
port: as_string(outbound.server_port),
|
|
id: as_string(outbound.uuid),
|
|
aid: as_string(outbound.alter_id || 0),
|
|
scy: as_string(outbound.security || "auto"),
|
|
net: "tcp",
|
|
type: "none",
|
|
host: "",
|
|
path: "",
|
|
tls: "",
|
|
sni: ""
|
|
};
|
|
|
|
if (type(outbound.tls) == "object" && outbound.tls.enabled !== false) {
|
|
vmess.tls = "tls";
|
|
vmess.sni = as_string(outbound.tls.server_name);
|
|
if (type(outbound.tls.utls) == "object")
|
|
vmess.fp = as_string(outbound.tls.utls.fingerprint);
|
|
}
|
|
|
|
if (type(outbound.transport) == "object") {
|
|
vmess.net = as_string(outbound.transport.type || "tcp");
|
|
if (vmess.net == "ws") {
|
|
vmess.path = as_string(outbound.transport.path);
|
|
if (type(outbound.transport.headers) == "object")
|
|
vmess.host = as_string(outbound.transport.headers.Host || outbound.transport.headers.host);
|
|
}
|
|
else if (vmess.net == "grpc") {
|
|
vmess.path = as_string(outbound.transport.service_name);
|
|
}
|
|
else if (vmess.net == "http") {
|
|
vmess.path = as_string(outbound.transport.path);
|
|
if (type(outbound.transport.host) == "array" && length(outbound.transport.host) > 0)
|
|
vmess.host = join(",", outbound.transport.host);
|
|
else
|
|
vmess.host = as_string(outbound.transport.host);
|
|
}
|
|
}
|
|
|
|
let encoded = base64_encode(sprintf("%J", vmess));
|
|
return encoded == "" ? "" : "vmess://" + encoded;
|
|
}
|
|
|
|
function serialize_outbound_link(outbound) {
|
|
if (type(outbound) != "object")
|
|
return "";
|
|
|
|
let outbound_type = as_string(outbound.type);
|
|
if (outbound_type == "vless")
|
|
return serialize_vless(outbound);
|
|
if (outbound_type == "trojan")
|
|
return serialize_trojan(outbound);
|
|
if (outbound_type == "shadowsocks")
|
|
return serialize_shadowsocks(outbound);
|
|
if (outbound_type == "socks")
|
|
return serialize_socks(outbound);
|
|
if (outbound_type == "http")
|
|
return serialize_http(outbound);
|
|
if (outbound_type == "hysteria2")
|
|
return serialize_hysteria2(outbound);
|
|
if (outbound_type == "tuic")
|
|
return serialize_tuic(outbound);
|
|
if (outbound_type == "vmess")
|
|
return serialize_vmess(outbound);
|
|
return "";
|
|
}
|
|
|
|
function is_copyable_link(value) {
|
|
value = lc(as_string(value));
|
|
let prefixes = [
|
|
"vless://", "vmess://", "trojan://", "ss://", "ssr://",
|
|
"hysteria2://", "hy2://", "tuic://",
|
|
"socks4://", "socks4a://", "socks5://",
|
|
"http://", "https://"
|
|
];
|
|
for (let prefix in prefixes) {
|
|
if (starts_with(value, prefix))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function populate_subscription_links(subscription) {
|
|
if (type(subscription) != "object" || type(subscription.outbounds) != "array")
|
|
return 0;
|
|
|
|
let changed = 0;
|
|
for (let outbound in subscription.outbounds) {
|
|
if (type(outbound) != "object" || is_copyable_link(outbound.share_link))
|
|
continue;
|
|
|
|
let link = serialize_outbound_link(outbound);
|
|
if (link == "")
|
|
continue;
|
|
|
|
outbound.share_link = link;
|
|
changed++;
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
function populate_subscription_file(path) {
|
|
path = as_string(path);
|
|
let data = fs.readfile(path);
|
|
if (data == null)
|
|
return false;
|
|
|
|
let subscription;
|
|
try {
|
|
subscription = json(data);
|
|
}
|
|
catch (e) {
|
|
return false;
|
|
}
|
|
|
|
if (populate_subscription_links(subscription) == 0)
|
|
return true;
|
|
|
|
let stamp = clock();
|
|
let tmp_path = sprintf("%s.%d.%d.tmp", path, stamp[0], stamp[1]);
|
|
if (fs.writefile(tmp_path, sprintf("%J\n", subscription)) == null)
|
|
return false;
|
|
|
|
system("chmod 600 " + shell_quote(tmp_path) + " >/dev/null 2>&1");
|
|
if (!fs.rename(tmp_path, path)) {
|
|
fs.unlink(tmp_path);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function populate_subscription_dir(path) {
|
|
path = as_string(path);
|
|
let ok = true;
|
|
for (let file_path in fs.glob(path + "/*.json"))
|
|
if (!populate_subscription_file(file_path))
|
|
ok = false;
|
|
|
|
return ok;
|
|
}
|
|
|
|
return {
|
|
serialize_outbound_link,
|
|
is_copyable_link,
|
|
populate_subscription_links,
|
|
populate_subscription_file,
|
|
populate_subscription_dir
|
|
}; |