From 3aacd26b796fd913bd7259cb38a54db6fe9db139 Mon Sep 17 00:00:00 2001 From: xream Date: Sat, 13 Jan 2024 10:28:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=BE=93=E5=87=BA?= =?UTF-8?q?=E5=88=B0=20sing-box;=20=E6=96=87=E4=BB=B6=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20``=20;=20=E8=84=9A=E6=9C=AC=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20`ProxyUtils.yaml`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/main.yml | 1 - backend/package.json | 2 +- backend/src/core/proxy-utils/index.js | 5 +- .../src/core/proxy-utils/processors/index.js | 18 +- .../core/proxy-utils/producers/clashmeta.js | 3 +- .../src/core/proxy-utils/producers/index.js | 2 + .../core/proxy-utils/producers/sing-box.js | 682 ++++++++++++++++++ backend/src/restful/node-info.js | 4 +- backend/src/restful/preview.js | 20 +- backend/src/restful/sync.js | 16 +- backend/src/utils/platform.js | 2 + 11 files changed, 729 insertions(+), 26 deletions(-) create mode 100644 backend/src/core/proxy-utils/producers/sing-box.js diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c1327c5..ba65ca1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,7 +37,6 @@ jobs: - name: Bundle run: | cd backend - pnpm i -D estrella pnpm run bundle - id: tag name: Generate release tag diff --git a/backend/package.json b/backend/package.json index 81178bc..cbd2979 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "sub-store", - "version": "2.14.147", + "version": "2.14.148", "description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.", "main": "src/main.js", "scripts": { diff --git a/backend/src/core/proxy-utils/index.js b/backend/src/core/proxy-utils/index.js index 7130280..4a046a9 100644 --- a/backend/src/core/proxy-utils/index.js +++ b/backend/src/core/proxy-utils/index.js @@ -1,3 +1,4 @@ +import YAML from 'static-js-yaml'; import download from '@/utils/download'; import { isIPv4, isIPv6, isValidPortNumber } from '@/utils'; import PROXY_PROCESSORS, { ApplyProcessor } from './processors'; @@ -59,7 +60,6 @@ function parse(raw) { $.error(`Failed to parse line: ${line}`); } } - return proxies; } @@ -193,6 +193,7 @@ export const ProxyUtils = { isIPv4, isIPv6, isIP, + yaml: YAML, }; function tryParse(parser, line) { @@ -218,7 +219,7 @@ function lastParse(proxy) { proxy.port = parseInt(proxy.port, 10); } if (proxy.server) { - proxy.server = proxy.server + proxy.server = `${proxy.server}` .trim() .replace(/^\[/, '') .replace(/\]$/, ''); diff --git a/backend/src/core/proxy-utils/processors/index.js b/backend/src/core/proxy-utils/processors/index.js index e1c0b6e..a96f909 100644 --- a/backend/src/core/proxy-utils/processors/index.js +++ b/backend/src/core/proxy-utils/processors/index.js @@ -325,9 +325,9 @@ function ScriptOperator(script, targetPlatform, $arguments, source) { return $server }) } else { - let $content = input + let { $content, $files } = input ${script} - return $content + return { $content, $files } } }`, @@ -658,13 +658,14 @@ async function ApplyFilter(filter, objs) { try { selected = await filter.func(objs); } catch (err) { - // print log and skip this filter - $.error(`Cannot apply filter ${filter.name}\n Reason: ${err}`); let funcErr = ''; let funcErrMsg = `${err.message ?? err}`; if (funcErrMsg.includes('$server is not defined')) { funcErr = ''; } else { + $.error( + `Cannot apply filter ${filter.name}(function filter)! Reason: ${err}`, + ); funcErr = `执行 function filter 失败 ${funcErrMsg}; `; } try { @@ -693,17 +694,18 @@ async function ApplyOperator(operator, objs) { const output_ = await operator.func(output); if (output_) output = output_; } catch (err) { - $.error( - `Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`, - ); let funcErr = ''; let funcErrMsg = `${err.message ?? err}`; if ( funcErrMsg.includes('$server is not defined') || - funcErrMsg.includes('$content is not defined') + funcErrMsg.includes('$content is not defined') || + funcErrMsg.includes('$files is not defined') ) { funcErr = ''; } else { + $.error( + `Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`, + ); funcErr = `执行 function operator 失败 ${funcErrMsg}; `; } try { diff --git a/backend/src/core/proxy-utils/producers/clashmeta.js b/backend/src/core/proxy-utils/producers/clashmeta.js index fc27ae5..d3d964a 100644 --- a/backend/src/core/proxy-utils/producers/clashmeta.js +++ b/backend/src/core/proxy-utils/producers/clashmeta.js @@ -2,9 +2,10 @@ import { isPresent } from '@/core/proxy-utils/producers/utils'; export default function ClashMeta_Producer() { const type = 'ALL'; - const produce = (proxies, type) => { + const produce = (proxies, type, opts = {}) => { const list = proxies .filter((proxy) => { + if (opts['include-unsupported-proxy']) return true; if (proxy.type === 'snell' && String(proxy.version) === '4') { return false; } diff --git a/backend/src/core/proxy-utils/producers/index.js b/backend/src/core/proxy-utils/producers/index.js index 0e87ec1..6a96d5c 100644 --- a/backend/src/core/proxy-utils/producers/index.js +++ b/backend/src/core/proxy-utils/producers/index.js @@ -9,6 +9,7 @@ import V2Ray_Producer from './v2ray'; import QX_Producer from './qx'; import ShadowRocket_Producer from './shadowrocket'; import Surfboard_Producer from './surfboard'; +import singbox_Producer from './sing-box'; function JSON_Producer() { const type = 'ALL'; @@ -29,4 +30,5 @@ export default { Stash: Stash_Producer(), ShadowRocket: ShadowRocket_Producer(), Surfboard: Surfboard_Producer(), + 'sing-box': singbox_Producer(), }; diff --git a/backend/src/core/proxy-utils/producers/sing-box.js b/backend/src/core/proxy-utils/producers/sing-box.js new file mode 100644 index 0000000..d62c0aa --- /dev/null +++ b/backend/src/core/proxy-utils/producers/sing-box.js @@ -0,0 +1,682 @@ +import ClashMeta_Producer from './clashmeta'; +import $ from '@/core/app'; + +const tfoParser = (proxy, parsedProxy) => { + parsedProxy.tcp_fast_open = false; + if (proxy.tfo) parsedProxy.tcp_fast_open = true; + if (proxy.tcp_fast_open) parsedProxy.tcp_fast_open = true; + if (proxy['tcp-fast-open']) parsedProxy.tcp_fast_open = true; + if (!parsedProxy.tcp_fast_open) delete parsedProxy.tcp_fast_open; +}; + +const smuxParser = (smux, proxy) => { + if (!smux || !smux.enabled) return; + proxy.multiplex = { enabled: true }; + proxy.multiplex.protocol = smux.protocol; + if (smux['max-connections']) + proxy.multiplex.max_connections = parseInt( + `${smux['max-connections']}`, + 10, + ); + if (smux['max-streams']) + proxy.multiplex.max_streams = parseInt(`${smux['max-streams']}`, 10); + if (smux['min-streams']) + proxy.multiplex.min_streams = parseInt(`${smux['min-streams']}`, 10); + if (smux.padding) proxy.multiplex.padding = true; +}; + +const wsParser = (proxy, parsedProxy) => { + const transport = { type: 'ws', headers: {} }; + if (proxy['ws-opts']) { + const { path: wsPath = '', headers: wsHeaders = {} } = proxy['ws-opts']; + if (wsPath !== '') transport.path = `${wsPath}`; + if (Object.keys(wsHeaders).length > 0) { + const headers = {}; + for (const key of Object.keys(wsHeaders)) { + let value = wsHeaders[key]; + if (value === '') continue; + if (!Array.isArray(value)) value = [`${value}`]; + if (value.length > 0) headers[key] = value; + } + const { Host: wsHost } = headers; + if (wsHost.length === 1) + for (const item of `Host:${wsHost[0]}`.split('\n')) { + const [key, value] = item.split(':'); + if (value.trim() === '') continue; + headers[key.trim()] = value.trim().split(','); + } + transport.headers = headers; + } + } + if (proxy['ws-headers']) { + const headers = {}; + for (const key of Object.keys(proxy['ws-headers'])) { + let value = proxy['ws-headers'][key]; + if (value === '') continue; + if (!Array.isArray(value)) value = [`${value}`]; + if (value.length > 0) headers[key] = value; + } + const { Host: wsHost } = headers; + if (wsHost.length === 1) + for (const item of `Host:${wsHost[0]}`.split('\n')) { + const [key, value] = item.split(':'); + if (value.trim() === '') continue; + headers[key.trim()] = value.trim().split(','); + } + for (const key of Object.keys(headers)) + transport.headers[key] = headers[key]; + } + if (proxy['ws-path'] && proxy['ws-path'] !== '') + transport.path = `${proxy['ws-path']}`; + if (transport.path) { + const reg = /^(.*?)(?:\?ed=(\d+))?$/; + // eslint-disable-next-line no-unused-vars + const [_, path = '', ed = ''] = reg.exec(transport.path); + transport.path = path; + if (ed !== '') { + transport.early_data_header_name = 'Sec-WebSocket-Protocol'; + transport.max_early_data = parseInt(ed, 10); + } + } + + if (parsedProxy.tls.insecure) + parsedProxy.tls.server_name = transport.headers.Host[0]; + if (proxy['ws-opts'] && proxy['ws-opts']['v2ray-http-upgrade']) { + transport.type = 'httpupgrade'; + if (transport.headers.Host) { + transport.host = transport.headers.Host[0]; + delete transport.headers.Host; + } + if (transport.max_early_data) delete transport.max_early_data; + if (transport.early_data_header_name) + delete transport.early_data_header_name; + } + for (const key of Object.keys(transport.headers)) { + const value = transport.headers[key]; + if (value.length === 1) transport.headers[key] = value[0]; + } + parsedProxy.transport = transport; +}; + +const h1Parser = (proxy, parsedProxy) => { + const transport = { type: 'http', headers: {} }; + if (proxy['http-opts']) { + const { + method = '', + path: h1Path = '', + headers: h1Headers = {}, + } = proxy['http-opts']; + if (method !== '') transport.method = method; + if (Array.isArray(h1Path)) { + transport.path = `${h1Path[0]}`; + } else if (h1Path !== '') transport.path = `${h1Path}`; + for (const key of Object.keys(h1Headers)) { + let value = h1Headers[key]; + if (value === '') continue; + if (key.toLowerCase() === 'host') { + let host = value; + if (!Array.isArray(host)) + host = `${host}`.split(',').map((i) => i.trim()); + if (host.length > 0) transport.host = host; + continue; + } + if (!Array.isArray(value)) + value = `${value}`.split(',').map((i) => i.trim()); + if (value.length > 0) transport.headers[key] = value; + } + } + if (proxy['http-host'] && proxy['http-host'] !== '') { + let host = proxy['http-host']; + if (!Array.isArray(host)) + host = `${host}`.split(',').map((i) => i.trim()); + if (host.length > 0) transport.host = host; + } + if (!transport.host) return; + if (proxy['http-path'] && proxy['http-path'] !== '') { + const path = proxy['http-path']; + if (Array.isArray(path)) { + transport.path = `${path[0]}`; + } else if (path !== '') transport.path = `${path}`; + } + if (parsedProxy.tls.insecure) + parsedProxy.tls.server_name = transport.host[0]; + if (transport.host.length === 1) transport.host = transport.host[0]; + for (const key of Object.keys(transport.headers)) { + const value = transport.headers[key]; + if (value.length === 1) transport.headers[key] = value[0]; + } + parsedProxy.transport = transport; +}; + +const h2Parser = (proxy, parsedProxy) => { + const transport = { type: 'http' }; + if (proxy['h2-opts']) { + let { host = '', path = '' } = proxy['h2-opts']; + if (path !== '') transport.path = `${path}`; + if (host !== '') { + if (!Array.isArray(host)) + host = `${host}`.split(',').map((i) => i.trim()); + if (host.length > 0) transport.host = host; + } + } + if (proxy['h2-host'] && proxy['h2-host'] !== '') { + let host = proxy['h2-host']; + if (!Array.isArray(host)) + host = `${host}`.split(',').map((i) => i.trim()); + if (host.length > 0) transport.host = host; + } + if (proxy['h2-path'] && proxy['h2-path'] !== '') + transport.path = `${proxy['h2-path']}`; + parsedProxy.tls.enabled = true; + if (parsedProxy.tls.insecure) + parsedProxy.tls.server_name = transport.host[0]; + if (transport.host.length === 1) transport.host = transport.host[0]; + parsedProxy.transport = transport; +}; + +const grpcParser = (proxy, parsedProxy) => { + const transport = { type: 'grpc' }; + if (proxy['grpc-opts']) { + const serviceName = proxy['grpc-opts']['grpc-service-name']; + if (serviceName && serviceName !== '') + transport.service_name = serviceName; + } + parsedProxy.transport = transport; +}; + +const tlsParser = (proxy, parsedProxy) => { + if (proxy.tls) parsedProxy.tls.enabled = true; + if (proxy.servername && proxy.servername !== '') + parsedProxy.tls.server_name = proxy.servername; + if (proxy.peer && proxy.peer !== '') + parsedProxy.tls.server_name = proxy.peer; + if (proxy.sni && proxy.sni !== '') parsedProxy.tls.server_name = proxy.sni; + if (proxy['skip-cert-verify']) parsedProxy.tls.insecure = true; + if (proxy.insecure) parsedProxy.tls.insecure = true; + if (proxy['disable-sni']) parsedProxy.tls.disable_sni = true; + if (typeof proxy.alpn === 'string') { + parsedProxy.tls.alpn = [proxy.alpn]; + } else if (Array.isArray(proxy.alpn)) parsedProxy.tls.alpn = proxy.alpn; + if (proxy.ca) parsedProxy.tls.certificate_path = `${proxy.ca}`; + if (proxy.ca_str) parsedProxy.tls.certificate = proxy.ca_sStr; + if (proxy['ca-str']) parsedProxy.tls.certificate = proxy['ca-str']; + if (proxy['client-fingerprint'] && proxy['client-fingerprint'] !== '') + parsedProxy.tls.utls = { + enabled: true, + fingerprint: proxy['client-fingerprint'], + }; + if (proxy['reality-opts']) { + parsedProxy.tls.reality = { enabled: true }; + if (proxy['reality-opts']['public-key']) + parsedProxy.tls.reality.public_key = + proxy['reality-opts']['public-key']; + if (proxy['reality-opts']['short-id']) + parsedProxy.tls.reality.short_id = + proxy['reality-opts']['short-id']; + } + if (!parsedProxy.tls.enabled) delete parsedProxy.tls; +}; + +const httpParser = (proxy = {}) => { + const parsedProxy = { + tag: proxy.name, + type: 'http', + server: proxy.server, + server_port: parseInt(`${proxy.port}`, 10), + tls: { enabled: false, server_name: proxy.server, insecure: false }, + }; + if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535) + throw 'invalid port'; + if (proxy.username) parsedProxy.username = proxy.username; + if (proxy.password) parsedProxy.password = proxy.password; + if (proxy.headers) { + parsedProxy.headers = {}; + for (const k of Object.keys(proxy.headers)) { + parsedProxy.headers[k] = `${proxy.headers[k]}`; + } + if (Object.keys(parsedProxy.headers).length === 0) + delete parsedProxy.headers; + } + if (proxy['fast-open']) parsedProxy.udp_fragment = true; + tfoParser(proxy, parsedProxy); + tlsParser(proxy, parsedProxy); + return parsedProxy; +}; + +const socks5Parser = (proxy = {}) => { + const parsedProxy = { + tag: proxy.name, + type: 'socks', + server: proxy.server, + server_port: parseInt(`${proxy.port}`, 10), + password: proxy.password, + version: '5', + }; + if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535) + throw 'invalid port'; + if (proxy.username) parsedProxy.username = proxy.username; + if (proxy.password) parsedProxy.password = proxy.password; + if (proxy.uot) parsedProxy.udp_over_tcp = true; + if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true; + if (proxy['fast-open']) parsedProxy.udp_fragment = true; + tfoParser(proxy, parsedProxy); + return parsedProxy; +}; + +const ssParser = (proxy = {}) => { + const parsedProxy = { + tag: proxy.name, + type: 'shadowsocks', + server: proxy.server, + server_port: parseInt(`${proxy.port}`, 10), + method: proxy.cipher, + password: proxy.password, + }; + if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535) + throw 'invalid port'; + if (proxy.uot) parsedProxy.udp_over_tcp = true; + if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true; + if (proxy['fast-open']) parsedProxy.udp_fragment = true; + tfoParser(proxy, parsedProxy); + smuxParser(proxy.smux, parsedProxy); + if (proxy.plugin) { + const optArr = []; + if (proxy.plugin === 'obfs') { + parsedProxy.plugin = 'obfs-local'; + parsedProxy.plugin_opts = ''; + if (proxy['obfs-host']) + proxy['plugin-opts'].host = proxy['obfs-host']; + Object.keys(proxy['plugin-opts']).forEach((k) => { + switch (k) { + case 'mode': + optArr.push(`obfs=${proxy['plugin-opts'].mode}`); + break; + case 'host': + optArr.push(`obfs-host=${proxy['plugin-opts'].host}`); + break; + default: + optArr.push(`${k}=${proxy['plugin-opts'][k]}`); + break; + } + }); + } + if (proxy.plugin === 'v2ray-plugin') { + parsedProxy.plugin = 'v2ray-plugin'; + if (proxy['ws-host']) proxy['plugin-opts'].host = proxy['ws-host']; + if (proxy['ws-path']) proxy['plugin-opts'].path = proxy['ws-path']; + Object.keys(proxy['plugin-opts']).forEach((k) => { + switch (k) { + case 'tls': + if (proxy['plugin-opts'].tls) optArr.push('tls'); + break; + case 'host': + optArr.push(`host=${proxy['plugin-opts'].host}`); + break; + case 'path': + optArr.push(`path=${proxy['plugin-opts'].path}`); + break; + case 'headers': + optArr.push( + `headers=${JSON.stringify( + proxy['plugin-opts'].headers, + )}`, + ); + break; + case 'mux': + if (proxy['plugin-opts'].mux) + parsedProxy.multiplex = { enabled: true }; + break; + default: + optArr.push(`${k}=${proxy['plugin-opts'][k]}`); + } + }); + } + parsedProxy.plugin_opts = optArr.join(';'); + } + + return parsedProxy; +}; +// eslint-disable-next-line no-unused-vars +const ssrParser = (proxy = {}) => { + const parsedProxy = { + tag: proxy.name, + type: 'shadowsocksr', + server: proxy.server, + server_port: parseInt(`${proxy.port}`, 10), + method: proxy.cipher, + password: proxy.password, + obfs: proxy.obfs, + protocol: proxy.protocol, + }; + if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535) + throw 'invalid port'; + if (proxy['obfs-param']) parsedProxy.obfs_param = proxy['obfs-param']; + if (proxy['protocol-param'] && proxy['protocol-param'] !== '') + parsedProxy.protocol_param = proxy['protocol-param']; + if (proxy['fast-open']) parsedProxy.udp_fragment = true; + tfoParser(proxy, parsedProxy); + smuxParser(proxy.smux, parsedProxy); + return parsedProxy; +}; + +const vmessParser = (proxy = {}) => { + const parsedProxy = { + tag: proxy.name, + type: 'vmess', + server: proxy.server, + server_port: parseInt(`${proxy.port}`, 10), + uuid: proxy.uuid, + security: proxy.cipher, + alter_id: parseInt(`${proxy.alterId}`, 10), + tls: { enabled: false, server_name: proxy.server, insecure: false }, + }; + if ( + [ + 'auto', + 'none', + 'zero', + 'aes-128-gcm', + 'chacha20-poly1305', + 'aes-128-ctr', + ].indexOf(parsedProxy.security) === -1 + ) + parsedProxy.security = 'auto'; + if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535) + throw 'invalid port'; + if (proxy.xudp) parsedProxy.packet_encoding = 'xudp'; + if (proxy['fast-open']) parsedProxy.udp_fragment = true; + if (proxy.network === 'ws') wsParser(proxy, parsedProxy); + if (proxy.network === 'h2') h2Parser(proxy, parsedProxy); + if (proxy.network === 'http') h1Parser(proxy, parsedProxy); + if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy); + + tfoParser(proxy, parsedProxy); + tlsParser(proxy, parsedProxy); + smuxParser(proxy.smux, parsedProxy); + return parsedProxy; +}; + +const vlessParser = (proxy = {}) => { + const parsedProxy = { + tag: proxy.name, + type: 'vless', + server: proxy.server, + server_port: parseInt(`${proxy.port}`, 10), + uuid: proxy.uuid, + tls: { enabled: false, server_name: proxy.server, insecure: false }, + }; + if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535) + throw 'invalid port'; + if (proxy['fast-open']) parsedProxy.udp_fragment = true; + if (proxy.flow === 'xtls-rprx-vision') parsedProxy.flow = proxy.flow; + if (proxy.network === 'ws') wsParser(proxy, parsedProxy); + if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy); + + tfoParser(proxy, parsedProxy); + smuxParser(proxy.smux, parsedProxy); + tlsParser(proxy, parsedProxy); + return parsedProxy; +}; +const trojanParser = (proxy = {}) => { + const parsedProxy = { + tag: proxy.name, + type: 'trojan', + server: proxy.server, + server_port: parseInt(`${proxy.port}`, 10), + password: proxy.password, + tls: { enabled: true, server_name: proxy.server, insecure: false }, + }; + if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535) + throw 'invalid port'; + if (proxy['fast-open']) parsedProxy.udp_fragment = true; + if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy); + if (proxy.network === 'ws') wsParser(proxy, parsedProxy); + + tfoParser(proxy, parsedProxy); + tlsParser(proxy, parsedProxy); + smuxParser(proxy.smux, parsedProxy); + return parsedProxy; +}; +const hysteriaParser = (proxy = {}) => { + const parsedProxy = { + tag: proxy.name, + type: 'hysteria', + server: proxy.server, + server_port: parseInt(`${proxy.port}`, 10), + disable_mtu_discovery: false, + tls: { enabled: true, server_name: proxy.server, insecure: false }, + }; + if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535) + throw 'invalid port'; + if (proxy.auth_str) parsedProxy.auth_str = `${proxy.auth_str}`; + if (proxy['auth-str']) parsedProxy.auth_str = `${proxy['auth-str']}`; + if (proxy['fast-open']) parsedProxy.udp_fragment = true; + // eslint-disable-next-line no-control-regex + const reg = new RegExp('^[0-9]+[ \t]*[KMGT]*[Bb]ps$'); + if (reg.test(`${proxy.up}`)) { + parsedProxy.up = `${proxy.up}`; + } else { + parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10); + } + if (reg.test(`${proxy.down}`)) { + parsedProxy.down = `${proxy.down}`; + } else { + parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10); + } + if (proxy.obfs) parsedProxy.obfs = proxy.obfs; + if (proxy.recv_window_conn) + parsedProxy.recv_window_conn = proxy.recv_window_conn; + if (proxy['recv-window-conn']) + parsedProxy.recv_window_conn = proxy['recv-window-conn']; + if (proxy.recv_window) parsedProxy.recv_window = proxy.recv_window; + if (proxy['recv-window']) parsedProxy.recv_window = proxy['recv-window']; + if (proxy.disable_mtu_discovery) { + if (typeof proxy.disable_mtu_discovery === 'boolean') { + parsedProxy.disable_mtu_discovery = proxy.disable_mtu_discovery; + } else { + if (proxy.disable_mtu_discovery === 1) + parsedProxy.disable_mtu_discovery = true; + } + } + tlsParser(proxy, parsedProxy); + tfoParser(proxy, parsedProxy); + smuxParser(proxy.smux, parsedProxy); + return parsedProxy; +}; +const hysteria2Parser = (proxy = {}) => { + const parsedProxy = { + tag: proxy.name, + type: 'hysteria2', + server: proxy.server, + server_port: parseInt(`${proxy.port}`, 10), + password: proxy.password, + obfs: {}, + tls: { enabled: true, server_name: proxy.server, insecure: false }, + }; + if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535) + throw 'invalid port'; + if (proxy.up) parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10); + if (proxy.down) parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10); + if (proxy.obfs === 'salamander') parsedProxy.obfs.type = 'salamander'; + if (proxy['obfs-password']) + parsedProxy.obfs.password = proxy['obfs-password']; + if (!parsedProxy.obfs.type) delete parsedProxy.obfs; + tlsParser(proxy, parsedProxy); + tfoParser(proxy, parsedProxy); + smuxParser(proxy.smux, parsedProxy); + return parsedProxy; +}; +const tuic5Parser = (proxy = {}) => { + const parsedProxy = { + tag: proxy.name, + type: 'tuic', + server: proxy.server, + server_port: parseInt(`${proxy.port}`, 10), + uuid: proxy.uuid, + password: proxy.password, + tls: { enabled: true, server_name: proxy.server, insecure: false }, + }; + if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535) + throw 'invalid port'; + if (proxy['fast-open']) parsedProxy.udp_fragment = true; + if ( + proxy['congestion-controller'] && + proxy['congestion-controller'] !== 'cubic' + ) + parsedProxy.congestion_control = proxy['congestion-controller']; + if (proxy['udp-relay-mode'] && proxy['udp-relay-mode'] !== 'native') + parsedProxy.udp_relay_mode = proxy['udp-relay-mode']; + if (proxy['reduce-rtt']) parsedProxy.zero_rtt_handshake = true; + if (proxy['udp-over-stream']) parsedProxy.udp_over_stream = true; + if (proxy['heartbeat-interval']) + parsedProxy.heartbeat = `${proxy['heartbeat-interval']}ms`; + tfoParser(proxy, parsedProxy); + tlsParser(proxy, parsedProxy); + smuxParser(proxy.smux, parsedProxy); + return parsedProxy; +}; + +const wireguardParser = (proxy = {}) => { + const parsedProxy = { + tag: proxy.name, + type: 'wireguard', + server: proxy.server, + server_port: parseInt(`${proxy.port}`, 10), + local_address: [proxy.ip, proxy.ipv6], + private_key: proxy['private-key'], + peer_public_key: proxy['public-key'], + pre_shared_key: proxy['pre-shared-key'], + reserved: [], + }; + if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535) + throw 'invalid port'; + if (proxy['fast-open']) parsedProxy.udp_fragment = true; + if (typeof proxy.reserved === 'string') { + parsedProxy.reserved.push(proxy.reserved); + } else { + for (const r of proxy.reserved) parsedProxy.reserved.push(r); + } + if (proxy.peers && proxy.peers.length > 0) { + parsedProxy.peers = []; + for (const p of proxy.peers) { + const peer = { + server: p.server, + server_port: parseInt(`${p.port}`, 10), + public_key: p['public-key'], + allowed_ips: p.allowed_ips, + reserved: [], + }; + if (typeof p.reserved === 'string') { + peer.reserved.push(p.reserved); + } else { + for (const r of p.reserved) peer.reserved.push(r); + } + if (p['pre-shared-key']) peer.pre_shared_key = p['pre-shared-key']; + parsedProxy.peers.push(peer); + } + } + tfoParser(proxy, parsedProxy); + smuxParser(proxy.smux, parsedProxy); + return parsedProxy; +}; + +export default function singbox_Producer() { + const type = 'ALL'; + const produce = (proxies, type) => { + const list = []; + ClashMeta_Producer() + .produce(proxies, 'internal', { 'include-unsupported-proxy': true }) + .map((proxy) => { + try { + switch (proxy.type) { + case 'http': + list.push(httpParser(proxy)); + break; + case 'socks5': + if (proxy.tls) { + throw new Error( + `Platform sing-box does not support proxy type: ${proxy.type} with tls`, + ); + } else { + list.push(socks5Parser(proxy)); + } + break; + case 'ss': + if (proxy.plugin === 'shadow-tls') { + throw new Error( + `Platform sing-box does not support proxy type: ${proxy.type} with shadow-tls`, + ); + } else { + list.push(ssParser(proxy)); + } + break; + // case 'ssr': + // list.push(ssrParser(proxy)); + // break; + case 'vmess': + if ( + !proxy.network || + ['ws', 'grpc', 'h2', 'http'].includes( + proxy.network, + ) + ) { + list.push(vmessParser(proxy)); + } else { + throw new Error( + `Platform sing-box does not support proxy type: ${proxy.type} with network ${proxy.network}`, + ); + } + break; + case 'vless': + if ( + !proxy.flow || + ['xtls-rprx-vision'].includes(proxy.flow) + ) { + list.push(vlessParser(proxy)); + } else { + throw new Error( + `Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`, + ); + } + break; + case 'trojan': + if (!proxy.flow) { + list.push(trojanParser(proxy)); + } else { + throw new Error( + `Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`, + ); + } + break; + case 'hysteria': + list.push(hysteriaParser(proxy)); + break; + case 'hysteria2': + list.push(hysteria2Parser(proxy)); + break; + case 'tuic': + if (!proxy.token || proxy.token.length === 0) { + list.push(tuic5Parser(proxy)); + } else { + throw new Error( + `Platform sing-box does not support proxy type: TUIC v4`, + ); + } + break; + case 'wireguard': + list.push(wireguardParser(proxy)); + break; + default: + throw new Error( + `Platform sing-box does not support proxy type: ${proxy.type}`, + ); + } + } catch (e) { + // console.log(e); + $.error(e.message ?? e); + } + }); + return type === 'internal' ? list : JSON.stringify(list, null, 2); + }; + return { type, produce }; +} diff --git a/backend/src/restful/node-info.js b/backend/src/restful/node-info.js index 2630be4..17ddc77 100644 --- a/backend/src/restful/node-info.js +++ b/backend/src/restful/node-info.js @@ -22,10 +22,10 @@ async function getNodeInfo(req, res) { const info = await $http .get({ url: `http://ip-api.com/json/${encodeURIComponent( - proxy.server + `${proxy.server}` .trim() .replace(/^\[/, '') - .replace(/\]$/, '') + .replace(/\]$/, ''), )}?lang=${lang}`, headers: { 'User-Agent': diff --git a/backend/src/restful/preview.js b/backend/src/restful/preview.js index d287262..7c21bb3 100644 --- a/backend/src/restful/preview.js +++ b/backend/src/restful/preview.js @@ -58,19 +58,25 @@ async function previewFile(req, res) { } } // parse proxies - const original = (Array.isArray(content) ? content : [content]) - .flat() + const files = (Array.isArray(content) ? content : [content]).flat(); + const filesContent = files .filter((i) => i != null && i !== '') .join('\n'); // apply processors - const processed = await ProxyUtils.process( - original, - file.process || [], - ); + const processed = + Array.isArray(file.process) && file.process.length > 0 + ? await ProxyUtils.process( + { $files: files, $content: filesContent }, + file.process, + ) + : filesContent; // produce - success(res, { original, processed }); + success(res, { + original: filesContent, + processed: processed?.$content ?? '', + }); } catch (err) { $.error(err.message ?? err); failed( diff --git a/backend/src/restful/sync.js b/backend/src/restful/sync.js index 3f29a91..069195c 100644 --- a/backend/src/restful/sync.js +++ b/backend/src/restful/sync.js @@ -418,12 +418,20 @@ async function produceArtifact({ raw.push(file.content); } } - let content = (Array.isArray(raw) ? raw : [raw]) - .flat() + const files = (Array.isArray(raw) ? raw : [raw]).flat(); + const filesContent = files .filter((i) => i != null && i !== '') .join('\n'); - content = await ProxyUtils.process(content, file.process || []); - return content ?? ''; + + // apply processors + const processed = + Array.isArray(file.process) && file.process.length > 0 + ? await ProxyUtils.process( + { $files: files, $content: filesContent }, + file.process, + ) + : filesContent; + return processed?.$content ?? ''; } } diff --git a/backend/src/utils/platform.js b/backend/src/utils/platform.js index 80f262e..570f2a7 100644 --- a/backend/src/utils/platform.js +++ b/backend/src/utils/platform.js @@ -32,6 +32,8 @@ export function getPlatformFromHeaders(headers) { return 'Clash'; } else if (ua.indexOf('v2ray') !== -1) { return 'V2Ray'; + } else if (ua.indexOf('sing-box') !== -1) { + return 'sing-box'; } else { return 'JSON'; }