mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-27 23:52:51 +08:00
450 lines
14 KiB
Ucode
450 lines
14 KiB
Ucode
/*
|
|
* SPDX-License-Identifier: GPL-2.0-only
|
|
*
|
|
* Every protocol's parse_<scheme>_uri()
|
|
* used to live in the top-level parse_uri.uc. They are now grouped
|
|
* here so the dispatch (parser/uri.uc), the validation
|
|
* (parser/validator.uc), and the canonical normalisation
|
|
* (parser/normalize.uc) can each be edited in isolation.
|
|
*
|
|
* Each parser returns a FLAT UCI-key dict - the key names UCI uses,
|
|
* produced through the mapping table (parser/mapping.uc) so a protocol
|
|
* option has one definition. That is the parser's output shape, not the
|
|
* pipeline's: normalize() turns it into a canonical Node, the policy step
|
|
* and the Repository work on that Node, and the Repository flattens it
|
|
* back to UCI keys on the way out. Nothing downstream of normalize() sees
|
|
* these flat dicts.
|
|
*
|
|
* `features` is the sing-box feature map (with_quic / with_utls / ...),
|
|
* `log` a logging callback, both injected so the parsers stay free of
|
|
* ubus / uci.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
import { urldecode, urlencode } from 'luci.http';
|
|
|
|
import { decodeBase64Str, isEmpty, parseURL } from '../homeproxy-pro.uc';
|
|
|
|
/* https://shadowsocks.org/guide/sip008.html */
|
|
export function parse_sip008_uri(uri) {
|
|
return {
|
|
label: uri.remarks,
|
|
type: 'shadowsocks',
|
|
address: uri.server,
|
|
port: uri.server_port,
|
|
shadowsocks_encrypt_method: uri.method,
|
|
password: uri.password,
|
|
shadowsocks_plugin: uri.plugin,
|
|
shadowsocks_plugin_opts: uri.plugin_opts
|
|
};
|
|
};
|
|
|
|
/* https://github.com/anytls/anytls-go/blob/v0.0.8/docs/uri_scheme.md */
|
|
export function parse_anytls_uri(uri) {
|
|
uri = split(trim(uri), '://');
|
|
|
|
const url = parseURL('http://' + uri[1]) || {};
|
|
const params = url.searchParams || {};
|
|
|
|
return {
|
|
label: url.hash ? urldecode(url.hash) : null,
|
|
type: 'anytls',
|
|
address: url.hostname,
|
|
port: url.port,
|
|
password: urldecode(url.username),
|
|
tls: '1',
|
|
tls_sni: params.sni,
|
|
tls_insecure: (params.insecure === '1') ? '1' : '0'
|
|
};
|
|
};
|
|
|
|
export function parse_http_uri(uri, features, log) {
|
|
uri = split(trim(uri), '://');
|
|
|
|
const url = parseURL('http://' + uri[1]) || {};
|
|
|
|
return {
|
|
label: url.hash ? urldecode(url.hash) : null,
|
|
type: 'http',
|
|
address: url.hostname,
|
|
port: url.port,
|
|
username: url.username ? urldecode(url.username) : null,
|
|
password: url.password ? urldecode(url.password) : null,
|
|
tls: (uri[0] === 'https') ? '1' : '0'
|
|
};
|
|
};
|
|
|
|
export function parse_hysteria_uri(uri, features, log) {
|
|
uri = split(trim(uri), '://');
|
|
|
|
const url = parseURL('http://' + uri[1]) || {};
|
|
const params = url.searchParams || {};
|
|
|
|
if (!features.with_quic || (params.protocol && params.protocol !== 'udp')) {
|
|
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
|
|
if (!features.with_quic)
|
|
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
|
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
label: url.hash ? urldecode(url.hash) : null,
|
|
type: 'hysteria',
|
|
address: url.hostname,
|
|
port: url.port,
|
|
hysteria_protocol: params.protocol || 'udp',
|
|
hysteria_auth_type: params.auth ? 'string' : null,
|
|
hysteria_auth_payload: params.auth,
|
|
hysteria_obfs_password: params.obfsParam,
|
|
hysteria_down_mbps: params.downmbps,
|
|
hysteria_up_mbps: params.upmbps,
|
|
tls: '1',
|
|
tls_insecure: (params.insecure in ['true', '1']) ? '1' : '0',
|
|
tls_sni: params.peer,
|
|
tls_alpn: params.alpn
|
|
};
|
|
};
|
|
|
|
/* https://v2.hysteria.network/docs/developers/URI-Scheme/ */
|
|
export function parse_hysteria2_uri(uri, features, log) {
|
|
uri = split(trim(uri), '://');
|
|
|
|
const url = parseURL('http://' + uri[1]) || {};
|
|
const params = url.searchParams || {};
|
|
|
|
if (!features.with_quic) {
|
|
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
|
|
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
label: url.hash ? urldecode(url.hash) : null,
|
|
type: 'hysteria2',
|
|
address: url.hostname,
|
|
port: url.port,
|
|
password: url.username ? (
|
|
urldecode(url.username + (url.password ? (':' + url.password) : ''))
|
|
) : null,
|
|
hysteria_obfs_type: params.obfs,
|
|
hysteria_obfs_password: params['obfs-password'],
|
|
tls: '1',
|
|
tls_insecure: (params.insecure === '1') ? '1' : '0',
|
|
tls_sni: params.sni
|
|
};
|
|
};
|
|
|
|
/* Surge Snell share link: snell://host:port?psk=..&obfs=http&obfs-host=..#name */
|
|
export function parse_snell_uri(uri) {
|
|
uri = split(trim(uri), '://');
|
|
|
|
const url = parseURL('http://' + uri[1]) || {};
|
|
const params = url.searchParams || {};
|
|
|
|
return {
|
|
label: url.hash ? urldecode(url.hash) : null,
|
|
type: 'snell',
|
|
address: url.hostname,
|
|
port: url.port,
|
|
password: url.username ? urldecode(url.username)
|
|
: (params.psk ? urldecode(params.psk) : null),
|
|
snell_version: params.version || '4',
|
|
snell_userkey: params.userkey,
|
|
snell_obfs_mode: (params.obfs === 'http') ? 'http' : null,
|
|
snell_obfs_host: params['obfs-host'],
|
|
snell_reuse: (params.reuse === '1') ? '1' : '0'
|
|
};
|
|
};
|
|
|
|
export function parse_socks_uri(uri) {
|
|
uri = split(trim(uri), '://');
|
|
|
|
const url = parseURL('http://' + uri[1]) || {};
|
|
|
|
return {
|
|
label: url.hash ? urldecode(url.hash) : null,
|
|
type: 'socks',
|
|
address: url.hostname,
|
|
port: url.port,
|
|
username: url.username ? urldecode(url.username) : null,
|
|
password: url.password ? urldecode(url.password) : null,
|
|
socks_version: (match(uri[0], /4/)) ? '4' : '5'
|
|
};
|
|
};
|
|
|
|
export function parse_ss_uri(uri) {
|
|
uri = split(trim(uri), '://');
|
|
|
|
/* "Lovely" Shadowrocket format */
|
|
const ss_suri = split(uri[1], '#');
|
|
let ss_slabel = '';
|
|
if (length(ss_suri) <= 2) {
|
|
if (length(ss_suri) === 2)
|
|
ss_slabel = '#' + urlencode(ss_suri[1]);
|
|
if (decodeBase64Str(ss_suri[0]))
|
|
uri[1] = decodeBase64Str(ss_suri[0]) + ss_slabel;
|
|
}
|
|
|
|
/* Legacy format is not supported, it should be never appeared in modern subscriptions */
|
|
/* https://github.com/shadowsocks/shadowsocks-org/commit/78ca46cd6859a4e9475953ed34a2d301454f579e */
|
|
|
|
/* SIP002 format https://shadowsocks.org/guide/sip002.html */
|
|
const url = parseURL('http://' + uri[1]) || {};
|
|
|
|
let ss_userinfo = {};
|
|
if (url.username && url.password)
|
|
/* User info encoded with URIComponent */
|
|
ss_userinfo = [url.username, urldecode(url.password)];
|
|
else if (url.username) {
|
|
/* User info encoded with base64. decodeBase64Str() returns null for
|
|
* anything that is not valid base64 (a '.' in the userinfo is
|
|
* enough), and ucode's split(null, ...) is null, so indexing it
|
|
* raised a ReferenceError. parse_uri() does not catch that: the RPC
|
|
* import path happens to, update_subscriptions.uc does not, so one
|
|
* malformed link aborted an entire subscription update. A link with
|
|
* no usable userinfo is simply not a node we can build. */
|
|
const decoded = decodeBase64Str(urldecode(url.username));
|
|
|
|
ss_userinfo = decoded ? split(decoded, ':', 2) : {};
|
|
}
|
|
|
|
let ss_plugin, ss_plugin_opts;
|
|
if (url.search && url.searchParams.plugin) {
|
|
const ss_plugin_info = split(url.searchParams.plugin, ';', 2);
|
|
ss_plugin = ss_plugin_info[0];
|
|
if (ss_plugin === 'simple-obfs')
|
|
/* Fix non-standard plugin name */
|
|
ss_plugin = 'obfs-local';
|
|
ss_plugin_opts = ss_plugin_info[1];
|
|
}
|
|
|
|
if (isEmpty(ss_userinfo[0]))
|
|
return null;
|
|
|
|
return {
|
|
label: url.hash ? urldecode(url.hash) : null,
|
|
type: 'shadowsocks',
|
|
address: url.hostname,
|
|
port: url.port,
|
|
shadowsocks_encrypt_method: ss_userinfo[0],
|
|
password: ss_userinfo[1],
|
|
shadowsocks_plugin: ss_plugin,
|
|
shadowsocks_plugin_opts: ss_plugin_opts
|
|
};
|
|
};
|
|
|
|
/* https://p4gefau1t.github.io/trojan-go/developer/url/ */
|
|
export function parse_trojan_uri(uri) {
|
|
uri = split(trim(uri), '://');
|
|
|
|
const url = parseURL('http://' + uri[1]) || {};
|
|
const params = url.searchParams || {};
|
|
|
|
const config = {
|
|
label: url.hash ? urldecode(url.hash) : null,
|
|
type: 'trojan',
|
|
address: url.hostname,
|
|
port: url.port,
|
|
password: urldecode(url.username),
|
|
transport: (params.type !== 'tcp') ? params.type : null,
|
|
tls: '1',
|
|
tls_sni: params.sni
|
|
};
|
|
switch (params.type) {
|
|
case 'grpc':
|
|
config.grpc_servicename = params.serviceName;
|
|
break;
|
|
case 'ws':
|
|
config.ws_host = params.host ? urldecode(params.host) : null;
|
|
config.ws_path = params.path ? urldecode(params.path) : null;
|
|
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
|
|
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
|
|
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
|
|
config.ws_path = split(config.ws_path, '?ed=')[0];
|
|
}
|
|
break;
|
|
}
|
|
|
|
return config;
|
|
};
|
|
|
|
/* https://github.com/daeuniverse/dae/discussions/182 */
|
|
export function parse_tuic_uri(uri, features, log) {
|
|
uri = split(trim(uri), '://');
|
|
|
|
const url = parseURL('http://' + uri[1]) || {};
|
|
const params = url.searchParams || {};
|
|
|
|
if (!features.with_quic) {
|
|
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
|
|
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
|
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
label: url.hash ? urldecode(url.hash) : null,
|
|
type: 'tuic',
|
|
address: url.hostname,
|
|
port: url.port,
|
|
uuid: url.username,
|
|
password: url.password ? urldecode(url.password) : null,
|
|
tuic_congestion_control: params.congestion_control,
|
|
tuic_udp_relay_mode: params.udp_relay_mode,
|
|
tls: '1',
|
|
tls_sni: params.sni,
|
|
tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null,
|
|
};
|
|
};
|
|
|
|
/* https://github.com/XTLS/Xray-core/discussions/716 */
|
|
export function parse_vless_uri(uri, features, log) {
|
|
uri = split(trim(uri), '://');
|
|
|
|
const url = parseURL('http://' + uri[1]) || {};
|
|
const params = url.searchParams || {};
|
|
|
|
/* Unsupported protocol */
|
|
if (params.type === 'kcp') {
|
|
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
|
|
return null;
|
|
} else if (params.type === 'quic' && ((params.quicSecurity && params.quicSecurity !== 'none') || !features.with_quic)) {
|
|
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
|
|
if (!features.with_quic)
|
|
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
|
|
|
return null;
|
|
}
|
|
|
|
const config = {
|
|
label: url.hash ? urldecode(url.hash) : null,
|
|
type: 'vless',
|
|
address: url.hostname,
|
|
port: url.port,
|
|
uuid: url.username,
|
|
transport: (params.type !== 'tcp') ? params.type : null,
|
|
tls: (params.security in ['tls', 'xtls', 'reality']) ? '1' : '0',
|
|
tls_sni: params.sni,
|
|
tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null,
|
|
tls_reality: (params.security === 'reality') ? '1' : '0',
|
|
tls_reality_public_key: params.pbk ? urldecode(params.pbk) : null,
|
|
tls_reality_short_id: params.sid,
|
|
tls_utls: features.with_utls ? params.fp : null,
|
|
vless_flow: (params.security in ['tls', 'reality']) ? params.flow : null
|
|
};
|
|
switch (params.type) {
|
|
case 'grpc':
|
|
config.grpc_servicename = params.serviceName;
|
|
break;
|
|
case 'http':
|
|
case 'tcp':
|
|
if (params.type === 'http' || params.headerType === 'http') {
|
|
config.http_host = params.host ? split(urldecode(params.host), ',') : null;
|
|
config.http_path = params.path ? urldecode(params.path) : null;
|
|
}
|
|
break;
|
|
case 'httpupgrade':
|
|
config.httpupgrade_host = params.host ? urldecode(params.host) : null;
|
|
config.http_path = params.path ? urldecode(params.path) : null;
|
|
break;
|
|
case 'ws':
|
|
config.ws_host = params.host ? urldecode(params.host) : null;
|
|
config.ws_path = params.path ? urldecode(params.path) : null;
|
|
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
|
|
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
|
|
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
|
|
config.ws_path = split(config.ws_path, '?ed=')[0];
|
|
}
|
|
break;
|
|
}
|
|
|
|
return config;
|
|
};
|
|
|
|
/* https://github.com/2dust/v2rayN/wiki/Description-of-VMess-share-link */
|
|
export function parse_vmess_uri(uri, features, log) {
|
|
uri = split(trim(uri), '://');
|
|
|
|
/* "Lovely" shadowrocket format. `uri` is the split() result, so the old
|
|
* match(uri, /&/) could never fire - match() takes a string. */
|
|
if (length(uri) > 1 && match(uri[1], /&/)) {
|
|
log(sprintf('Skipping unsupported %s format.', uri[0]));
|
|
return null;
|
|
}
|
|
|
|
let payload;
|
|
try {
|
|
payload = json(decodeBase64Str(uri[1])) || {};
|
|
} catch(e) {
|
|
log(sprintf('Skipping unsupported %s format.', uri[0]));
|
|
return null;
|
|
}
|
|
|
|
if (payload.v != '2') {
|
|
log(sprintf('Skipping unsupported %s format.', uri[0]));
|
|
return null;
|
|
/* Unsupported protocol */
|
|
} else if (payload.net === 'kcp') {
|
|
log(sprintf('Skipping unsupported %s node: %s.', uri[0], payload.ps || payload.add));
|
|
return null;
|
|
} else if (payload.net === 'quic' && ((payload.type && payload.type !== 'none') || payload.path || !features.with_quic)) {
|
|
log(sprintf('Skipping unsupported %s node: %s.', uri[0], payload.ps || payload.add));
|
|
if (!features.with_quic)
|
|
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
|
|
|
return null;
|
|
}
|
|
/*
|
|
* https://www.v2fly.org/config/protocols/vmess.html#vmess-md5-%E8%AE%A4%E8%AF%81%E4%BF%A1%E6%81%AF-%E6%B7%98%E6%B1%B0%E6%9C%BA%E5%88%B6
|
|
* else if (payload.aid && int(payload.aid) !== 0) {
|
|
* log(sprintf('Skipping unsupported %s node: %s.', uri[0], payload.ps || payload.add));
|
|
* return null;
|
|
* }
|
|
*/
|
|
|
|
const config = {
|
|
label: payload.ps ? urldecode(payload.ps) : null,
|
|
type: 'vmess',
|
|
address: payload.add,
|
|
port: payload.port,
|
|
uuid: payload.id,
|
|
vmess_alterid: payload.aid,
|
|
vmess_encrypt: payload.scy || 'auto',
|
|
vmess_global_padding: '1',
|
|
transport: (payload.net !== 'tcp') ? payload.net : null,
|
|
tls: (payload.tls === 'tls') ? '1' : '0',
|
|
tls_sni: payload.sni || payload.host,
|
|
tls_alpn: payload.alpn ? split(payload.alpn, ',') : null,
|
|
tls_utls: features.with_utls ? payload.fp : null
|
|
};
|
|
switch (payload.net) {
|
|
case 'grpc':
|
|
config.grpc_servicename = payload.path;
|
|
break;
|
|
case 'h2':
|
|
case 'tcp':
|
|
if (payload.net === 'h2' || payload.type === 'http') {
|
|
config.transport = 'http';
|
|
config.http_host = payload.host ? split(payload.host, ',') : null;
|
|
config.http_path = payload.path;
|
|
}
|
|
break;
|
|
case 'httpupgrade':
|
|
config.httpupgrade_host = payload.host;
|
|
config.http_path = payload.path;
|
|
break;
|
|
case 'ws':
|
|
config.ws_host = payload.host;
|
|
config.ws_path = payload.path;
|
|
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
|
|
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
|
|
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
|
|
config.ws_path = split(config.ws_path, '?ed=')[0];
|
|
}
|
|
break;
|
|
}
|
|
|
|
return config;
|
|
}; |