mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-13 20:04:45 +08:00
220 lines
5.4 KiB
Ucode
220 lines
5.4 KiB
Ucode
/*
|
|
* SPDX-License-Identifier: GPL-2.0-only
|
|
*
|
|
* HomeProxy 订阅 parser helper。
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
function is_empty(ctx, value) {
|
|
if (type(ctx?.isEmpty) === 'function')
|
|
return ctx.isEmpty(value);
|
|
|
|
return !value || value === 'nil' || (type(value) in ['array', 'object'] && length(value) === 0);
|
|
}
|
|
|
|
function validate(ctx, kind, value) {
|
|
return type(ctx?.validation) === 'function' ? ctx.validation(kind, value) : false;
|
|
}
|
|
|
|
function log(ctx, ...args) {
|
|
if (type(ctx?.log) === 'function')
|
|
ctx.log(...args);
|
|
}
|
|
|
|
function singFeatures(ctx) {
|
|
return ctx?.sing_features || {};
|
|
}
|
|
|
|
function applyTlsCertPinPolicy(ctx, config, label, pin) {
|
|
if (type(ctx?.apply_tls_cert_pin_policy) === 'function')
|
|
ctx.apply_tls_cert_pin_policy(config, label, pin);
|
|
}
|
|
|
|
function parse_surge_proxy(ctx, line) {
|
|
let config;
|
|
|
|
const eq = index(line, '=');
|
|
if (eq < 0)
|
|
return null;
|
|
|
|
const label = trim(substr(line, 0, eq));
|
|
const rhs = trim(substr(line, eq + 1));
|
|
if (is_empty(ctx, label) || is_empty(ctx, rhs))
|
|
return null;
|
|
|
|
const tokens = [];
|
|
let cur = '';
|
|
let in_quote = false;
|
|
for (let i = 0; i < length(rhs); i++) {
|
|
const ch = substr(rhs, i, 1);
|
|
if (ch == '"')
|
|
in_quote = !in_quote;
|
|
else if (ch == ',' && !in_quote) {
|
|
push(tokens, trim(cur));
|
|
cur = '';
|
|
} else
|
|
cur += ch;
|
|
}
|
|
if (length(trim(cur)))
|
|
push(tokens, trim(cur));
|
|
|
|
if (length(tokens) < 3)
|
|
return null;
|
|
|
|
const surge_type = tokens[0];
|
|
const address = tokens[1];
|
|
const port = tokens[2];
|
|
const opts = {};
|
|
let last_key = null;
|
|
for (let i = 3; i < length(tokens); i++) {
|
|
const t = tokens[i];
|
|
const k = index(t, '=');
|
|
if (k < 0) {
|
|
if (last_key == 'port-hopping')
|
|
opts[last_key] = opts[last_key] + ',' + t;
|
|
continue;
|
|
}
|
|
|
|
let key = lc(trim(substr(t, 0, k)));
|
|
let val = trim(substr(t, k + 1));
|
|
if (length(val) >= 2 && substr(val, 0, 1) == '"' && substr(val, length(val) - 1, 1) == '"')
|
|
val = substr(val, 1, length(val) - 2);
|
|
opts[key] = val;
|
|
last_key = key;
|
|
}
|
|
|
|
function bval(v) { return (v == 'true') ? '1' : '0'; }
|
|
|
|
switch (surge_type) {
|
|
case 'trojan':
|
|
config = {
|
|
label: label,
|
|
type: 'trojan',
|
|
address: address,
|
|
port: port,
|
|
password: opts.password,
|
|
tls: '1',
|
|
tls_sni: opts.sni,
|
|
tls_insecure: opts['skip-cert-verify'] ? bval(opts['skip-cert-verify']) : '0',
|
|
tls_alpn: opts.alpn ? split(opts.alpn, ',') : null
|
|
};
|
|
if (opts.ws == 'true') {
|
|
config.transport = 'ws';
|
|
config.ws_path = opts['ws-path'];
|
|
if (opts['ws-headers']) {
|
|
const hm = match(opts['ws-headers'], /[Hh]ost\s*:\s*"?([^",]+)/);
|
|
if (hm)
|
|
config.ws_host = hm[1];
|
|
}
|
|
}
|
|
break;
|
|
case 'ss':
|
|
config = {
|
|
label: label,
|
|
type: 'shadowsocks',
|
|
address: address,
|
|
port: port,
|
|
shadowsocks_encrypt_method: opts['encrypt-method'],
|
|
password: opts.password
|
|
};
|
|
if (opts['udp-over-tcp'] == 'true') {
|
|
config.udp_over_tcp = '1';
|
|
config.udp_over_tcp_version = '2';
|
|
}
|
|
if (opts['shadow-tls-password'] && opts['shadow-tls-sni']) {
|
|
config.shadowtls_enabled = '1';
|
|
config.shadowtls_address = address;
|
|
config.shadowtls_port = port;
|
|
config.shadowtls_password = opts['shadow-tls-password'];
|
|
config.shadowtls_sni = opts['shadow-tls-sni'];
|
|
config.shadowtls_version = opts['shadow-tls-version'] || '3';
|
|
}
|
|
break;
|
|
case 'hysteria2':
|
|
case 'hy2':
|
|
if (!singFeatures(ctx).with_quic) {
|
|
log(ctx, sprintf('Skipping unsupported %s node: %s.', surge_type, label));
|
|
log(ctx, sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
|
return null;
|
|
}
|
|
config = {
|
|
label: label,
|
|
type: 'hysteria2',
|
|
address: address,
|
|
port: port,
|
|
password: opts.password,
|
|
hysteria_obfs_type: opts.obfs,
|
|
hysteria_obfs_password: opts['salamander-password'] || opts['obfs-password'],
|
|
tls: '1',
|
|
tls_sni: opts.sni,
|
|
tls_insecure: opts['skip-cert-verify'] ? bval(opts['skip-cert-verify']) : '0'
|
|
};
|
|
applyTlsCertPinPolicy(ctx, config, label, opts['server-cert-fingerprint-sha256']);
|
|
if (opts['port-hopping'])
|
|
config.hysteria_hopping_port = map(split(opts['port-hopping'], ','),
|
|
(seg) => replace(trim(seg), '-', ':'));
|
|
if (opts.alpn)
|
|
config.tls_alpn = split(opts.alpn, ',');
|
|
break;
|
|
case 'anytls':
|
|
config = {
|
|
label: label,
|
|
type: 'anytls',
|
|
address: address,
|
|
port: port,
|
|
password: opts.password,
|
|
tls: '1',
|
|
tls_sni: opts.sni,
|
|
tls_insecure: opts['skip-cert-verify'] ? bval(opts['skip-cert-verify']) : '0'
|
|
};
|
|
break;
|
|
default:
|
|
log(ctx, sprintf('Skipping unsupported Surge proxy type: %s (%s).', surge_type, label));
|
|
return null;
|
|
}
|
|
|
|
if (!is_empty(ctx, config)) {
|
|
if (config.address)
|
|
config.address = replace(config.address, /\[|\]/g, '');
|
|
|
|
if (!validate(ctx, 'host', config.address) || !validate(ctx, 'port', config.port)) {
|
|
log(ctx, sprintf('Skipping invalid %s node: %s.', config.type, config.label || 'NULL'));
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
export function parse_surge_subscription(ctx, body) {
|
|
const lines = split(body, '\n');
|
|
const nodes = [];
|
|
let in_proxies = false;
|
|
|
|
for (let i = 0; i < length(lines); i++) {
|
|
const ln = trim(lines[i]);
|
|
|
|
if (match(lines[i], /^[ \t]*proxies\s*:/)) {
|
|
in_proxies = true;
|
|
continue;
|
|
}
|
|
if (in_proxies && match(lines[i], /^[A-Za-z0-9_-]+\s*:/) && !match(lines[i], /^\s/))
|
|
in_proxies = false;
|
|
if (!in_proxies)
|
|
continue;
|
|
|
|
if (is_empty(ctx, ln) || substr(ln, 0, 1) == '#')
|
|
continue;
|
|
|
|
if (index(ln, '=') < 0)
|
|
continue;
|
|
|
|
const cfg = parse_surge_proxy(ctx, ln);
|
|
if (cfg)
|
|
push(nodes, cfg);
|
|
}
|
|
|
|
return nodes;
|
|
};
|