mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-27 02:11:19 +08:00
465 lines
12 KiB
Ucode
465 lines
12 KiB
Ucode
/* thanks for homeproxy */
|
|
|
|
import { mkstemp, popen } from 'fs';
|
|
|
|
/* Global variables START */
|
|
export const HM_DIR = '/etc/fchomo';
|
|
export const EXE_DIR = '/usr/libexec/fchomo';
|
|
export const SDL_DIR = '/usr/share/fchomo';
|
|
export const RUN_DIR = '/var/run/fchomo';
|
|
export const PRESET_OUTBOUND = [
|
|
'DIRECT',
|
|
'REJECT',
|
|
'REJECT-DROP',
|
|
'PASS',
|
|
'PASS-RULE',
|
|
'COMPATIBLE',
|
|
'GLOBAL'
|
|
];
|
|
export const RULES_LOGICAL_TYPE = [
|
|
'AND',
|
|
'OR',
|
|
'NOT'
|
|
];
|
|
/* Global variables END */
|
|
|
|
/* Utilities start */
|
|
/* Kanged from luci-app-commands */
|
|
export function shellQuote(s) {
|
|
return `'${replace(s, "'", "'\\''")}'`;
|
|
};
|
|
|
|
export function isBinary(str) {
|
|
for (let off = 0, byte = ord(str); off < length(str); byte = ord(str, ++off))
|
|
if (byte <= 8 || (byte >= 14 && byte <= 31))
|
|
return true;
|
|
|
|
return false;
|
|
};
|
|
|
|
export function executeCommand(infd, ...args) {
|
|
let outfd = mkstemp();
|
|
let errfd = mkstemp();
|
|
|
|
if (infd)
|
|
push(args, `<&${infd.fileno()}`);
|
|
|
|
const exitcode = system(`${join(' ', args)} >&${outfd.fileno()} 2>&${errfd.fileno()}`);
|
|
|
|
outfd.seek();
|
|
errfd.seek();
|
|
|
|
const stdout = outfd.read(1024 * 1024) ?? '';
|
|
const stderr = errfd.read(1024 * 1024) ?? '';
|
|
|
|
outfd.close();
|
|
errfd.close();
|
|
|
|
const binary = isBinary(stdout);
|
|
|
|
return {
|
|
command: join(' ', args),
|
|
stdout: binary ? null : stdout,
|
|
stderr,
|
|
exitcode,
|
|
binary
|
|
};
|
|
};
|
|
|
|
export function yqRead(flags, command, content) {
|
|
let infd = mkstemp();
|
|
|
|
if (content) {
|
|
content = trim(content);
|
|
content = replace(content, /\r\n?/g, '\n');
|
|
if (!match(content, /\n$/))
|
|
content += '\n';
|
|
}
|
|
infd.write(content);
|
|
|
|
infd.seek();
|
|
const out = executeCommand(infd, 'yq', flags, shellQuote(command));
|
|
infd.close();
|
|
|
|
return out.stdout;
|
|
};
|
|
|
|
export function yqReadFile(flags, command, filepath) {
|
|
const out = executeCommand(null, 'yq', flags, shellQuote(command), shellQuote(filepath));
|
|
|
|
return out.stdout;
|
|
};
|
|
/* Utilities end */
|
|
|
|
/* String helper start */
|
|
export function isEmpty(res) { // no false, 0, NaN
|
|
if (res == null || res in ['']) return true; // null, ''
|
|
if (type(res) in ['array', 'object']) return length(res) === 0; // empty Array/Object
|
|
return false;
|
|
};
|
|
|
|
export function strToBool(str) {
|
|
return (str === '1') || null;
|
|
};
|
|
|
|
export function strToInt(str) {
|
|
if (isEmpty(str))
|
|
return null;
|
|
|
|
return !match(str, /^\d+$/) ? str : int(str) ?? null;
|
|
};
|
|
|
|
export function bytesizeToByte(str) {
|
|
if (isEmpty(str))
|
|
return null;
|
|
|
|
let bytes = 0;
|
|
let arr = match(str, /^(\d+)(k|m|g)?b?$/);
|
|
if (arr) {
|
|
if (arr[2] === 'k') {
|
|
bytes = strToInt(arr[1]) * 1024;
|
|
} else if (arr[2] === 'm') {
|
|
bytes = strToInt(arr[1]) * 1048576;
|
|
} else if (arr[2] === 'g') {
|
|
bytes = strToInt(arr[1]) * 1073741824;
|
|
} else
|
|
bytes = strToInt(arr[1]);
|
|
}
|
|
|
|
return bytes;
|
|
};
|
|
export function durationToSecond(str) {
|
|
if (isEmpty(str))
|
|
return null;
|
|
|
|
let seconds = 0;
|
|
let arr = match(str, /^(\d+)(s|m|h|d)?$/);
|
|
if (arr) {
|
|
if (arr[2] === 's') {
|
|
seconds = strToInt(arr[1]);
|
|
} else if (arr[2] === 'm') {
|
|
seconds = strToInt(arr[1]) * 60;
|
|
} else if (arr[2] === 'h') {
|
|
seconds = strToInt(arr[1]) * 3600;
|
|
} else if (arr[2] === 'd') {
|
|
seconds = strToInt(arr[1]) * 86400;
|
|
} else
|
|
seconds = strToInt(arr[1]);
|
|
}
|
|
|
|
return seconds;
|
|
};
|
|
|
|
export function arrToObj(res) {
|
|
if (isEmpty(res))
|
|
return null;
|
|
|
|
let object;
|
|
if (type(res) === 'array') {
|
|
object = {};
|
|
map(res, (e) => {
|
|
if (type(e) === 'array')
|
|
object[e[0]] = e[1];
|
|
});
|
|
} else
|
|
return res;
|
|
|
|
return object;
|
|
};
|
|
|
|
export function removeBlankAttrs(res) {
|
|
let content;
|
|
|
|
if (type(res) === 'object') {
|
|
content = {};
|
|
map(keys(res), (k) => {
|
|
if (type(res[k]) in ['array', 'object'])
|
|
content[k] = removeBlankAttrs(res[k]);
|
|
else if (res[k] !== null && res[k] !== '')
|
|
content[k] = res[k];
|
|
});
|
|
} else if (type(res) === 'array') {
|
|
content = [];
|
|
map(res, (k, i) => {
|
|
if (type(k) in ['array', 'object'])
|
|
push(content, removeBlankAttrs(k));
|
|
else if (k !== null && k !== '')
|
|
push(content, k);
|
|
});
|
|
} else
|
|
return res;
|
|
|
|
return content;
|
|
};
|
|
/* String helper end */
|
|
|
|
/* String universal start */
|
|
export function parseVlessEncryption(payload, side) {
|
|
if (isEmpty(payload))
|
|
return null;
|
|
|
|
let content = json(trim(payload));
|
|
|
|
let required = join('.', [
|
|
content.method,
|
|
content.xormode,
|
|
side === 'server' ? content.ticket : side === 'client' ? content.rtt : null
|
|
]);
|
|
|
|
return required +
|
|
(isEmpty(content.paddings) ? '' : '.' + join('.', content.paddings)) + // Optional
|
|
(isEmpty(content.keypairs) ? '' : '.' + join('.', map(content.keypairs, e => e[side]))); // Required
|
|
};
|
|
|
|
export function parseListener(cfg) {
|
|
return {
|
|
name: cfg['.name'],
|
|
type: cfg.type,
|
|
|
|
listen: cfg.listen || '::',
|
|
port: strToInt(cfg.port),
|
|
"routing-mark": strToInt(cfg.routing_mark) || null,
|
|
rule: cfg.rule,
|
|
proxy: cfg.proxy, // raw data need post-processing
|
|
|
|
/* HTTP / SOCKS / Mieru / VMess / VLESS / Trojan / AnyTLS / Tuic / Hysteria2 / ShadowQUIC / TrustTunnel */
|
|
users: (cfg.type in ['http', 'socks', 'mixed', 'vmess', 'vless', 'trojan', 'shadowquic', 'trusttunnel']) ? [
|
|
(cfg.username || cfg.vmess_uuid) ? {
|
|
/* HTTP / SOCKS / Trojan / ShadowQUIC / TrustTunnel */
|
|
username: cfg.username,
|
|
password: cfg.password,
|
|
|
|
/* VMess / VLESS */
|
|
uuid: cfg.vmess_uuid,
|
|
flow: cfg.vless_flow,
|
|
alterId: strToInt(cfg.vmess_alterid)
|
|
} : null
|
|
/*{
|
|
}*/
|
|
] : ((cfg.type in ['mieru', 'anytls', 'tuic', 'hysteria2']) ? {
|
|
/* Mieru / AnyTLS / Hysteria2 */
|
|
...arrToObj([[cfg.username, cfg.password]]),
|
|
|
|
/* Tuic */
|
|
...arrToObj([[cfg.uuid, cfg.password]])
|
|
} : null),
|
|
|
|
/* Shadowsocks */
|
|
cipher: cfg.shadowsocks_chipher,
|
|
password: cfg.shadowsocks_password,
|
|
|
|
/* Mieru */
|
|
transport: cfg.mieru_transport,
|
|
"traffic-pattern": cfg.mieru_traffic_pattern,
|
|
"user-hint-is-mandatory": strToBool(cfg.mieru_user_hint_is_mandatory),
|
|
|
|
/* Sudoku */
|
|
key: cfg.sudoku_key,
|
|
"aead-method": replace(cfg.sudoku_aead_method || '', 'chacha20-ietf-poly1305', 'chacha20-poly1305') || null,
|
|
"padding-min": strToInt(cfg.sudoku_padding_min),
|
|
"padding-max": strToInt(cfg.sudoku_padding_max),
|
|
"table-type": cfg.sudoku_table_type,
|
|
"custom-tables": cfg.sudoku_custom_tables,
|
|
"handshake-timeout": strToInt(cfg.sudoku_handshake_timeout) ?? null,
|
|
"enable-pure-downlink": (cfg.sudoku_enable_pure_downlink === '0') ? false : null,
|
|
...(cfg.type === 'sudoku' ? {
|
|
httpmask: (cfg.sudoku_http_mask === '0') ? { disable: true } : {
|
|
disable: false,
|
|
mode: cfg.sudoku_http_mask_mode,
|
|
"path-root": cfg.sudoku_path_root,
|
|
}
|
|
} : {}),
|
|
fallback: (cfg.sudoku_http_mask === '0') ? null : cfg.sudoku_fallback,
|
|
|
|
/* Snell */
|
|
psk: cfg.snell_psk,
|
|
version: cfg.snell_version,
|
|
|
|
/* VMess / VLESS */
|
|
decryption: cfg.vless_decryption === '1' ? parseVlessEncryption(cfg.vless_encryption_hmpayload, 'server') : null,
|
|
|
|
/* Trojan */
|
|
"ss-option": cfg.trojan_ss_enabled === '1' ? {
|
|
enabled: true,
|
|
method: cfg.trojan_ss_chipher,
|
|
password: cfg.trojan_ss_password
|
|
} : null,
|
|
|
|
/* AnyTLS */
|
|
"padding-scheme": cfg.anytls_padding_scheme,
|
|
|
|
/* Tuic */
|
|
"authentication-timeout": durationToSecond(cfg.tuic_authentication_timeout),
|
|
"max-udp-relay-packet-size": strToInt(cfg.tuic_max_udp_relay_packet_size),
|
|
|
|
/* Hysteria2 */
|
|
up: strToInt(cfg.hysteria_up_mbps),
|
|
down: strToInt(cfg.hysteria_down_mbps),
|
|
"ignore-client-bandwidth": strToBool(cfg.hysteria_ignore_client_bandwidth),
|
|
obfs: cfg.hysteria_obfs_type,
|
|
"obfs-password": cfg.hysteria_obfs_password,
|
|
"obfs-min-packet-size": strToInt(cfg.hysteria_obfs_min_packet_size),
|
|
"obfs-max-packet-size": strToInt(cfg.hysteria_obfs_max_packet_size),
|
|
masquerade: cfg.hysteria_masquerade,
|
|
"realm-opts": cfg.hysteria2_realm === '1' ? {
|
|
enable: true,
|
|
"server-url": cfg.hysteria2_realm_server_url,
|
|
token: cfg.hysteria2_realm_token,
|
|
"realm-id": cfg.hysteria2_realm_id,
|
|
"stun-servers": cfg.hysteria2_realm_stun_servers,
|
|
// @TLS of server-url
|
|
//sni,
|
|
//alpn,
|
|
//"skip-cert-verify",
|
|
//fingerprint,
|
|
//certificate,
|
|
//"private-key"
|
|
} : null,
|
|
|
|
/* Hysteria2 Realmserver */
|
|
token: cfg.hysteria2_realmserver_token,
|
|
"max-realms": strToInt(cfg.hysteria2_realmserver_max_realms),
|
|
"max-realms-per-ip": strToInt(cfg.hysteria2_realmserver_max_realms_per_ip),
|
|
"trusted-proxy-header": cfg.hysteria2_realmserver_trusted_proxy_header,
|
|
"realm-name-pattern": cfg.hysteria2_realmserver_realm_name_pattern,
|
|
|
|
/* ShadowQUIC */
|
|
"quic-versions": cfg.shadowquic_quic_versions,
|
|
"zero-rtt": strToBool(cfg.shadowquic_zero_rtt),
|
|
"jls-upstream": cfg.type === 'shadowquic' ? {
|
|
addr: cfg.plugin_opts_handshake_dest,
|
|
sni: cfg.tls_sni,
|
|
proxy: cfg.plugin_opts_dest_proxy, // raw data need post-processing
|
|
"rate-limit": strToInt(cfg.plugin_opts_rate_limit),
|
|
"quic-version-probe": strToBool(cfg.plugin_opts_quic_version_probe)
|
|
} : null,
|
|
// @# cwnd: 10 # default: 32,
|
|
// @# max-datagram-frame-size: 1400,
|
|
// @# recv-window-conn: 0,
|
|
// @# recv-window: 0,
|
|
// @# disable-mtu-discovery: false,
|
|
|
|
/* TrustTunnel */
|
|
|
|
/* Tunnel */
|
|
target: cfg.tunnel_target,
|
|
|
|
/* Extra fields */
|
|
"congestion-controller": cfg.congestion_controller,
|
|
"bbr-profile": cfg.bbr_profile,
|
|
"max-idle-time": durationToSecond(cfg.max_idle_time),
|
|
|
|
network: cfg.network,
|
|
udp: cfg.udp === '0' ? false : true,
|
|
|
|
/* Plugin fields */
|
|
...(cfg.plugin === '1' ? (
|
|
cfg.plugin_type === 'obfs' ? (
|
|
// obfs-simple
|
|
cfg.type === 'snell' ? {
|
|
// snell
|
|
"obfs-opts": {
|
|
mode: cfg.plugin_opts_obfsmode,
|
|
host: cfg.plugin_opts_host
|
|
}
|
|
} : {
|
|
// shadowsocks
|
|
"simple-obfs": {
|
|
enable: true,
|
|
mode: cfg.plugin_opts_obfsmode
|
|
}
|
|
}
|
|
) : cfg.plugin_type === 'shadow-tls' ? {
|
|
// shadow-tls
|
|
"shadow-tls": {
|
|
enable: true,
|
|
version: strToInt(cfg.plugin_opts_shadowtls_version),
|
|
...(strToInt(cfg.plugin_opts_shadowtls_version) >= 3 ? {
|
|
users: [
|
|
{
|
|
name: 1,
|
|
password: cfg.plugin_opts_thetlspassword
|
|
}
|
|
],
|
|
} : { password: cfg.plugin_opts_thetlspassword }),
|
|
handshake: {
|
|
dest: cfg.plugin_opts_handshake_dest
|
|
}
|
|
}
|
|
} : cfg.plugin_type === 'restls' ? {
|
|
// restls
|
|
"res-tls": {
|
|
enable: true,
|
|
dest: cfg.plugin_opts_handshake_dest,
|
|
password: cfg.plugin_opts_thetlspassword,
|
|
"restls-script": cfg.plugin_opts_restls_script,
|
|
//"min-record-len": 0,
|
|
proxy: cfg.plugin_opts_dest_proxy // raw data need post-processing
|
|
}
|
|
} : cfg.plugin_type === 'jls' ? {
|
|
// jls
|
|
"jls-config": {
|
|
enable: true,
|
|
users: [
|
|
{
|
|
username: cfg.plugin_opts_thetlsusername,
|
|
password: cfg.plugin_opts_thetlspassword
|
|
}
|
|
],
|
|
dest: cfg.plugin_opts_handshake_dest,
|
|
sni: cfg.tls_sni,
|
|
alpn: cfg.tls_alpn,
|
|
proxy: cfg.plugin_opts_dest_proxy, // raw data need post-processing
|
|
"rate-limit": strToInt(cfg.plugin_opts_rate_limit)
|
|
}
|
|
} : {}
|
|
) : {}),
|
|
|
|
/* TLS fields */
|
|
...(cfg.allow_insecure === '1' ? { "allow-insecure": true } : cfg.tls === '1' ? {
|
|
alpn: cfg.tls_alpn,
|
|
...(cfg.tls_reality === '1' ? {
|
|
"reality-config": {
|
|
dest: cfg.tls_reality_dest,
|
|
"private-key": cfg.tls_reality_private_key,
|
|
"short-id": cfg.tls_reality_short_id,
|
|
"server-names": cfg.tls_reality_server_names
|
|
}
|
|
} : {
|
|
certificate: cfg.tls_cert_path,
|
|
"private-key": cfg.tls_key_path
|
|
}),
|
|
"client-auth-type": cfg.tls_client_auth_type,
|
|
"client-auth-cert": cfg.tls_client_auth_cert_path,
|
|
"ech-key": cfg.tls_ech_key,
|
|
} : {}),
|
|
|
|
/* Transport fields */
|
|
...(cfg.transport_enabled === '1' ? {
|
|
"grpc-service-name": cfg.transport_type === 'grpc' ? cfg.transport_grpc_servicename : null,
|
|
"ws-path": cfg.transport_type === 'ws' ? cfg.transport_path : null,
|
|
"xhttp-config": cfg.transport_type === 'xhttp' ? {
|
|
path: cfg.transport_path,
|
|
host: cfg.transport_host,
|
|
mode: cfg.transport_xhttp_mode,
|
|
"no-sse-header": strToBool(cfg.transport_xhttp_no_sse_header),
|
|
"sc-max-buffered-posts": strToInt(cfg.transport_xhttp_sc_max_buffered_posts) || null,
|
|
"sc-stream-up-server-secs": cfg.transport_xhttp_sc_stream_up_server_secs,
|
|
"sc-max-each-post-bytes": strToInt(cfg.transport_xhttp_sc_max_each_post_bytes) || null,
|
|
// @其他的配置
|
|
} : null
|
|
} : {}),
|
|
|
|
/* Multiplex fields */
|
|
"mux-option": cfg.smux_enabled === '1' ? {
|
|
padding: strToBool(cfg.smux_padding),
|
|
brutal: cfg.smux_brutal === '1' ? {
|
|
enabled: true,
|
|
up: strToInt(cfg.smux_brutal_up) || null, // Mbps
|
|
down: strToInt(cfg.smux_brutal_down) || null // Mbps
|
|
} : null
|
|
} : null
|
|
}
|
|
};
|
|
/* String universal end */
|