diff --git a/backend/package.json b/backend/package.json index c14eb91..990d663 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "sub-store", - "version": "2.14.154", + "version": "2.14.155", "description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.", "main": "src/main.js", "scripts": { @@ -21,6 +21,7 @@ "http-proxy-middleware": "^2.0.6", "js-base64": "^3.7.2", "lodash": "^4.17.21", + "nunjucks": "^3.2.4", "request": "^2.88.2", "requests": "^0.3.0", "semver": "^7.3.7", diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 6520789..35358fd 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -26,6 +26,9 @@ dependencies: lodash: specifier: ^4.17.21 version: registry.npmmirror.com/lodash@4.17.21 + nunjucks: + specifier: ^3.2.4 + version: registry.npmmirror.com/nunjucks@3.2.4 request: specifier: ^2.88.2 version: registry.npmmirror.com/request@2.88.2 @@ -162,11 +165,6 @@ packages: dev: true optional: true - /source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - dev: true - registry.npmmirror.com/@ampproject/remapping@2.2.0: resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.2.0.tgz} name: '@ampproject/remapping' @@ -2126,6 +2124,12 @@ packages: through: registry.npmmirror.com/through@2.3.8 dev: true + registry.npmmirror.com/a-sync-waterfall@1.0.1: + resolution: {integrity: sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz} + name: a-sync-waterfall + version: 1.0.1 + dev: false + registry.npmmirror.com/abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/abbrev/-/abbrev-1.1.1.tgz} name: abbrev @@ -2464,6 +2468,12 @@ packages: is-string: registry.npmmirror.com/is-string@1.0.7 dev: true + registry.npmmirror.com/asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/asap/-/asap-2.0.6.tgz} + name: asap + version: 2.0.6 + dev: false + registry.npmmirror.com/asn1.js@5.4.1: resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/asn1.js/-/asn1.js-5.4.1.tgz} name: asn1.js @@ -3471,6 +3481,13 @@ packages: engines: {node: '>= 6'} dev: true + registry.npmmirror.com/commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/commander/-/commander-5.1.0.tgz} + name: commander + version: 5.1.0 + engines: {node: '>= 6'} + dev: false + registry.npmmirror.com/commander@9.3.0: resolution: {integrity: sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/commander/-/commander-9.3.0.tgz} name: commander @@ -7379,6 +7396,23 @@ packages: engines: {node: '>=0.10.0'} dev: true + registry.npmmirror.com/nunjucks@3.2.4: + resolution: {integrity: sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/nunjucks/-/nunjucks-3.2.4.tgz} + name: nunjucks + version: 3.2.4 + engines: {node: '>= 6.9.0'} + hasBin: true + peerDependencies: + chokidar: ^3.3.0 + peerDependenciesMeta: + chokidar: + optional: true + dependencies: + a-sync-waterfall: registry.npmmirror.com/a-sync-waterfall@1.0.1 + asap: registry.npmmirror.com/asap@2.0.6 + commander: registry.npmmirror.com/commander@5.1.0 + dev: false + registry.npmmirror.com/oauth-sign@0.9.0: resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/oauth-sign/-/oauth-sign-0.9.0.tgz} name: oauth-sign @@ -9320,7 +9354,7 @@ packages: dependencies: acorn: registry.npmmirror.com/acorn@8.7.1 commander: registry.npmmirror.com/commander@2.20.3 - source-map: 0.6.1 + source-map: registry.npmmirror.com/source-map@0.6.1 source-map-support: registry.npmmirror.com/source-map-support@0.5.21 dev: true diff --git a/backend/src/core/proxy-utils/index.js b/backend/src/core/proxy-utils/index.js index 4a046a9..e20c9bb 100644 --- a/backend/src/core/proxy-utils/index.js +++ b/backend/src/core/proxy-utils/index.js @@ -139,7 +139,7 @@ async function process(proxies, operators = [], targetPlatform, source) { return proxies; } -function produce(proxies, targetPlatform, type) { +function produce(proxies, targetPlatform, type, opts = {}) { const producer = PROXY_PRODUCERS[targetPlatform]; if (!producer) { throw new Error(`Target platform: ${targetPlatform} is not supported!`); @@ -154,10 +154,10 @@ function produce(proxies, targetPlatform, type) { $.info(`Producing proxies for target: ${targetPlatform}`); if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') { let localPort = 10000; - return proxies + const list = proxies .map((proxy) => { try { - let line = producer.produce(proxy, type); + let line = producer.produce(proxy, type, opts); if ( line.length > 0 && line.includes('__SubStoreLocalPort__') @@ -179,10 +179,10 @@ function produce(proxies, targetPlatform, type) { return ''; } }) - .filter((line) => line.length > 0) - .join('\n'); + .filter((line) => line.length > 0); + return type === 'internal' ? list : list.join('\n'); } else if (producer.type === 'ALL') { - return producer.produce(proxies, type); + return producer.produce(proxies, type, opts); } } diff --git a/backend/src/core/proxy-utils/producers/clash.js b/backend/src/core/proxy-utils/producers/clash.js index 9d29ba8..42e5c76 100644 --- a/backend/src/core/proxy-utils/producers/clash.js +++ b/backend/src/core/proxy-utils/producers/clash.js @@ -2,136 +2,139 @@ import { isPresent } from '@/core/proxy-utils/producers/utils'; export default function Clash_Producer() { const type = 'ALL'; - const produce = (proxies) => { + const produce = (proxies, type, opts = {}) => { // VLESS XTLS is not supported by Clash // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532 // github.com/Dreamacro/clash/pull/2891/files // filter unsupported proxies // https://clash.wiki/configuration/outbound.html#shadowsocks - proxies = proxies.filter((proxy) => { - if ( - ![ - 'ss', - 'ssr', - 'vmess', - 'vless', - 'socks5', - 'http', - 'snell', - 'trojan', - 'wireguard', - ].includes(proxy.type) || - (proxy.type === 'ss' && + const list = proxies + .filter((proxy) => { + if (opts['include-unsupported-proxy']) return true; + if ( ![ - 'aes-128-gcm', - 'aes-192-gcm', - 'aes-256-gcm', - 'aes-128-cfb', - 'aes-192-cfb', - 'aes-256-cfb', - 'aes-128-ctr', - 'aes-192-ctr', - 'aes-256-ctr', - 'rc4-md5', - 'chacha20-ietf', - 'xchacha20', - 'chacha20-ietf-poly1305', - 'xchacha20-ietf-poly1305', - ].includes(proxy.cipher)) || - (proxy.type === 'snell' && String(proxy.version) === '4') || - (proxy.type === 'vless' && - (typeof proxy.flow !== 'undefined' || - proxy['reality-opts'])) - ) { - return false; - } - return true; - }); - return ( - 'proxies:\n' + - proxies - .map((proxy) => { - if (proxy.type === 'vmess') { - // handle vmess aead - if (isPresent(proxy, 'aead')) { - if (proxy.aead) { - proxy.alterId = 0; - } - delete proxy.aead; - } - if (isPresent(proxy, 'sni')) { - proxy.servername = proxy.sni; - delete proxy.sni; - } - // https://dreamacro.github.io/clash/configuration/outbound.html#vmess - if ( - isPresent(proxy, 'cipher') && - ![ - 'auto', - 'aes-128-gcm', - 'chacha20-poly1305', - 'none', - ].includes(proxy.cipher) - ) { - proxy.cipher = 'auto'; - } - } else if (proxy.type === 'wireguard') { - proxy.keepalive = - proxy.keepalive ?? proxy['persistent-keepalive']; - proxy['persistent-keepalive'] = proxy.keepalive; - proxy['preshared-key'] = - proxy['preshared-key'] ?? proxy['pre-shared-key']; - proxy['pre-shared-key'] = proxy['preshared-key']; - } else if (proxy.type === 'vless') { - if (isPresent(proxy, 'sni')) { - proxy.servername = proxy.sni; - delete proxy.sni; + 'ss', + 'ssr', + 'vmess', + 'vless', + 'socks5', + 'http', + 'snell', + 'trojan', + 'wireguard', + ].includes(proxy.type) || + (proxy.type === 'ss' && + ![ + 'aes-128-gcm', + 'aes-192-gcm', + 'aes-256-gcm', + 'aes-128-cfb', + 'aes-192-cfb', + 'aes-256-cfb', + 'aes-128-ctr', + 'aes-192-ctr', + 'aes-256-ctr', + 'rc4-md5', + 'chacha20-ietf', + 'xchacha20', + 'chacha20-ietf-poly1305', + 'xchacha20-ietf-poly1305', + ].includes(proxy.cipher)) || + (proxy.type === 'snell' && String(proxy.version) === '4') || + (proxy.type === 'vless' && + (typeof proxy.flow !== 'undefined' || + proxy['reality-opts'])) + ) { + return false; + } + return true; + }) + .map((proxy) => { + if (proxy.type === 'vmess') { + // handle vmess aead + if (isPresent(proxy, 'aead')) { + if (proxy.aead) { + proxy.alterId = 0; } + delete proxy.aead; } + if (isPresent(proxy, 'sni')) { + proxy.servername = proxy.sni; + delete proxy.sni; + } + // https://dreamacro.github.io/clash/configuration/outbound.html#vmess + if ( + isPresent(proxy, 'cipher') && + ![ + 'auto', + 'aes-128-gcm', + 'chacha20-poly1305', + 'none', + ].includes(proxy.cipher) + ) { + proxy.cipher = 'auto'; + } + } else if (proxy.type === 'wireguard') { + proxy.keepalive = + proxy.keepalive ?? proxy['persistent-keepalive']; + proxy['persistent-keepalive'] = proxy.keepalive; + proxy['preshared-key'] = + proxy['preshared-key'] ?? proxy['pre-shared-key']; + proxy['pre-shared-key'] = proxy['preshared-key']; + } else if (proxy.type === 'vless') { + if (isPresent(proxy, 'sni')) { + proxy.servername = proxy.sni; + delete proxy.sni; + } + } + if ( + ['vmess', 'vless'].includes(proxy.type) && + proxy.network === 'http' + ) { + let httpPath = proxy['http-opts']?.path; if ( - ['vmess', 'vless'].includes(proxy.type) && - proxy.network === 'http' + isPresent(proxy, 'http-opts.path') && + !Array.isArray(httpPath) ) { - let httpPath = proxy['http-opts']?.path; - if ( - isPresent(proxy, 'http-opts.path') && - !Array.isArray(httpPath) - ) { - proxy['http-opts'].path = [httpPath]; - } - let httpHost = proxy['http-opts']?.headers?.Host; - if ( - isPresent(proxy, 'http-opts.headers.Host') && - !Array.isArray(httpHost) - ) { - proxy['http-opts'].headers.Host = [httpHost]; - } + proxy['http-opts'].path = [httpPath]; } + let httpHost = proxy['http-opts']?.headers?.Host; if ( - ['trojan', 'tuic', 'hysteria', 'hysteria2'].includes( - proxy.type, - ) + isPresent(proxy, 'http-opts.headers.Host') && + !Array.isArray(httpHost) ) { - delete proxy.tls; + proxy['http-opts'].headers.Host = [httpHost]; } + } + if ( + ['trojan', 'tuic', 'hysteria', 'hysteria2'].includes( + proxy.type, + ) + ) { + delete proxy.tls; + } - if (proxy['tls-fingerprint']) { - proxy.fingerprint = proxy['tls-fingerprint']; - } - delete proxy['tls-fingerprint']; - delete proxy.subName; - delete proxy.collectionName; - if ( - ['grpc'].includes(proxy.network) && - proxy[`${proxy.network}-opts`] - ) { - delete proxy[`${proxy.network}-opts`]['_grpc-type']; - } - return ' - ' + JSON.stringify(proxy) + '\n'; - }) - .join('') - ); + if (proxy['tls-fingerprint']) { + proxy.fingerprint = proxy['tls-fingerprint']; + } + delete proxy['tls-fingerprint']; + delete proxy.subName; + delete proxy.collectionName; + if ( + ['grpc'].includes(proxy.network) && + proxy[`${proxy.network}-opts`] + ) { + delete proxy[`${proxy.network}-opts`]['_grpc-type']; + } + return proxy; + }); + return type === 'internal' + ? list + : 'proxies:\n' + + list + .map((proxy) => ' - ' + JSON.stringify(proxy) + '\n') + .join(''); }; return { type, produce }; } diff --git a/backend/src/core/proxy-utils/producers/index.js b/backend/src/core/proxy-utils/producers/index.js index 6a96d5c..79721de 100644 --- a/backend/src/core/proxy-utils/producers/index.js +++ b/backend/src/core/proxy-utils/producers/index.js @@ -7,7 +7,7 @@ import Loon_Producer from './loon'; import URI_Producer from './uri'; import V2Ray_Producer from './v2ray'; import QX_Producer from './qx'; -import ShadowRocket_Producer from './shadowrocket'; +import Shadowrocket_Producer from './shadowrocket'; import Surfboard_Producer from './surfboard'; import singbox_Producer from './sing-box'; @@ -19,6 +19,7 @@ function JSON_Producer() { export default { QX: QX_Producer(), + QuantumultX: QX_Producer(), Surge: Surge_Producer(), SurgeMac: SurgeMac_Producer(), Loon: Loon_Producer(), @@ -28,7 +29,8 @@ export default { V2Ray: V2Ray_Producer(), JSON: JSON_Producer(), Stash: Stash_Producer(), - ShadowRocket: ShadowRocket_Producer(), + Shadowrocket: Shadowrocket_Producer(), + ShadowRocket: Shadowrocket_Producer(), Surfboard: Surfboard_Producer(), 'sing-box': singbox_Producer(), }; diff --git a/backend/src/core/proxy-utils/producers/shadowrocket.js b/backend/src/core/proxy-utils/producers/shadowrocket.js index 75d0df0..84e29f9 100644 --- a/backend/src/core/proxy-utils/producers/shadowrocket.js +++ b/backend/src/core/proxy-utils/producers/shadowrocket.js @@ -2,162 +2,161 @@ import { isPresent } from '@/core/proxy-utils/producers/utils'; export default function ShadowRocket_Producer() { const type = 'ALL'; - const produce = (proxies) => { - return ( - 'proxies:\n' + - proxies - .filter((proxy) => { + 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; + } + return true; + }) + .map((proxy) => { + if (proxy.type === 'vmess') { + // handle vmess aead + if (isPresent(proxy, 'aead')) { + if (proxy.aead) { + proxy.alterId = 0; + } + delete proxy.aead; + } + if (isPresent(proxy, 'sni')) { + proxy.servername = proxy.sni; + delete proxy.sni; + } + // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400 + // https://stash.wiki/proxy-protocols/proxy-types#vmess if ( - proxy.type === 'snell' && - String(proxy.version) === '4' + isPresent(proxy, 'cipher') && + ![ + 'auto', + 'aes-128-gcm', + 'chacha20-poly1305', + 'none', + ].includes(proxy.cipher) ) { - return false; + proxy.cipher = 'auto'; } - return true; - }) - .map((proxy) => { - if (proxy.type === 'vmess') { - // handle vmess aead - if (isPresent(proxy, 'aead')) { - if (proxy.aead) { - proxy.alterId = 0; - } - delete proxy.aead; - } - if (isPresent(proxy, 'sni')) { - proxy.servername = proxy.sni; - delete proxy.sni; - } - // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400 - // https://stash.wiki/proxy-protocols/proxy-types#vmess - if ( - isPresent(proxy, 'cipher') && - ![ - 'auto', - 'aes-128-gcm', - 'chacha20-poly1305', - 'none', - ].includes(proxy.cipher) - ) { - proxy.cipher = 'auto'; - } - } else if (proxy.type === 'tuic') { - if (isPresent(proxy, 'alpn')) { - proxy.alpn = Array.isArray(proxy.alpn) - ? proxy.alpn - : [proxy.alpn]; - } else { - proxy.alpn = ['h3']; - } - if ( - isPresent(proxy, 'tfo') && - !isPresent(proxy, 'fast-open') - ) { - proxy['fast-open'] = proxy.tfo; - } - // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197 - if ( - (!proxy.token || proxy.token.length === 0) && - !isPresent(proxy, 'version') - ) { - proxy.version = 5; - } - } else if (proxy.type === 'hysteria') { - // auth_str 将会在未来某个时候删除 但是有的机场不规范 - if ( - isPresent(proxy, 'auth_str') && - !isPresent(proxy, 'auth-str') - ) { - proxy['auth-str'] = proxy['auth_str']; - } - if (isPresent(proxy, 'alpn')) { - proxy.alpn = Array.isArray(proxy.alpn) - ? proxy.alpn - : [proxy.alpn]; - } - if ( - isPresent(proxy, 'tfo') && - !isPresent(proxy, 'fast-open') - ) { - proxy['fast-open'] = proxy.tfo; - } - } else if (proxy.type === 'hysteria2') { - if ( - proxy['obfs-password'] && - proxy.obfs == 'salamander' - ) { - proxy.obfs = proxy['obfs-password']; - delete proxy['obfs-password']; - } - if (isPresent(proxy, 'alpn')) { - proxy.alpn = Array.isArray(proxy.alpn) - ? proxy.alpn - : [proxy.alpn]; - } - if ( - isPresent(proxy, 'tfo') && - !isPresent(proxy, 'fast-open') - ) { - proxy['fast-open'] = proxy.tfo; - } - } else if (proxy.type === 'wireguard') { - proxy.keepalive = - proxy.keepalive ?? proxy['persistent-keepalive']; - proxy['persistent-keepalive'] = proxy.keepalive; - proxy['preshared-key'] = - proxy['preshared-key'] ?? proxy['pre-shared-key']; - proxy['pre-shared-key'] = proxy['preshared-key']; - } else if (proxy.type === 'vless') { - if (isPresent(proxy, 'sni')) { - proxy.servername = proxy.sni; - delete proxy.sni; - } + } else if (proxy.type === 'tuic') { + if (isPresent(proxy, 'alpn')) { + proxy.alpn = Array.isArray(proxy.alpn) + ? proxy.alpn + : [proxy.alpn]; + } else { + proxy.alpn = ['h3']; } + if ( + isPresent(proxy, 'tfo') && + !isPresent(proxy, 'fast-open') + ) { + proxy['fast-open'] = proxy.tfo; + } + // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197 + if ( + (!proxy.token || proxy.token.length === 0) && + !isPresent(proxy, 'version') + ) { + proxy.version = 5; + } + } else if (proxy.type === 'hysteria') { + // auth_str 将会在未来某个时候删除 但是有的机场不规范 + if ( + isPresent(proxy, 'auth_str') && + !isPresent(proxy, 'auth-str') + ) { + proxy['auth-str'] = proxy['auth_str']; + } + if (isPresent(proxy, 'alpn')) { + proxy.alpn = Array.isArray(proxy.alpn) + ? proxy.alpn + : [proxy.alpn]; + } + if ( + isPresent(proxy, 'tfo') && + !isPresent(proxy, 'fast-open') + ) { + proxy['fast-open'] = proxy.tfo; + } + } else if (proxy.type === 'hysteria2') { + if (proxy['obfs-password'] && proxy.obfs == 'salamander') { + proxy.obfs = proxy['obfs-password']; + delete proxy['obfs-password']; + } + if (isPresent(proxy, 'alpn')) { + proxy.alpn = Array.isArray(proxy.alpn) + ? proxy.alpn + : [proxy.alpn]; + } + if ( + isPresent(proxy, 'tfo') && + !isPresent(proxy, 'fast-open') + ) { + proxy['fast-open'] = proxy.tfo; + } + } else if (proxy.type === 'wireguard') { + proxy.keepalive = + proxy.keepalive ?? proxy['persistent-keepalive']; + proxy['persistent-keepalive'] = proxy.keepalive; + proxy['preshared-key'] = + proxy['preshared-key'] ?? proxy['pre-shared-key']; + proxy['pre-shared-key'] = proxy['preshared-key']; + } else if (proxy.type === 'vless') { + if (isPresent(proxy, 'sni')) { + proxy.servername = proxy.sni; + delete proxy.sni; + } + } + if ( + ['vmess', 'vless'].includes(proxy.type) && + proxy.network === 'http' + ) { + let httpPath = proxy['http-opts']?.path; if ( - ['vmess', 'vless'].includes(proxy.type) && - proxy.network === 'http' + isPresent(proxy, 'http-opts.path') && + !Array.isArray(httpPath) ) { - let httpPath = proxy['http-opts']?.path; - if ( - isPresent(proxy, 'http-opts.path') && - !Array.isArray(httpPath) - ) { - proxy['http-opts'].path = [httpPath]; - } - let httpHost = proxy['http-opts']?.headers?.Host; - if ( - isPresent(proxy, 'http-opts.headers.Host') && - !Array.isArray(httpHost) - ) { - proxy['http-opts'].headers.Host = [httpHost]; - } + proxy['http-opts'].path = [httpPath]; } + let httpHost = proxy['http-opts']?.headers?.Host; + if ( + isPresent(proxy, 'http-opts.headers.Host') && + !Array.isArray(httpHost) + ) { + proxy['http-opts'].headers.Host = [httpHost]; + } + } - if ( - ['trojan', 'tuic', 'hysteria', 'hysteria2'].includes( - proxy.type, - ) - ) { - delete proxy.tls; - } + if ( + ['trojan', 'tuic', 'hysteria', 'hysteria2'].includes( + proxy.type, + ) + ) { + delete proxy.tls; + } - if (proxy['tls-fingerprint']) { - proxy.fingerprint = proxy['tls-fingerprint']; - } - delete proxy['tls-fingerprint']; - delete proxy.subName; - delete proxy.collectionName; - if ( - ['grpc'].includes(proxy.network) && - proxy[`${proxy.network}-opts`] - ) { - delete proxy[`${proxy.network}-opts`]['_grpc-type']; - } - return ' - ' + JSON.stringify(proxy) + '\n'; - }) - .join('') - ); + if (proxy['tls-fingerprint']) { + proxy.fingerprint = proxy['tls-fingerprint']; + } + delete proxy['tls-fingerprint']; + delete proxy.subName; + delete proxy.collectionName; + if ( + ['grpc'].includes(proxy.network) && + proxy[`${proxy.network}-opts`] + ) { + delete proxy[`${proxy.network}-opts`]['_grpc-type']; + } + return proxy; + }); + return type === 'internal' + ? list + : 'proxies:\n' + + list + .map((proxy) => { + return ' - ' + JSON.stringify(proxy) + '\n'; + }) + .join(''); }; return { type, produce }; } diff --git a/backend/src/core/proxy-utils/producers/sing-box.js b/backend/src/core/proxy-utils/producers/sing-box.js index d62c0aa..80b2122 100644 --- a/backend/src/core/proxy-utils/producers/sing-box.js +++ b/backend/src/core/proxy-utils/producers/sing-box.js @@ -582,7 +582,7 @@ const wireguardParser = (proxy = {}) => { export default function singbox_Producer() { const type = 'ALL'; - const produce = (proxies, type) => { + const produce = (proxies, type, opts = {}) => { const list = []; ClashMeta_Producer() .produce(proxies, 'internal', { 'include-unsupported-proxy': true }) @@ -610,9 +610,15 @@ export default function singbox_Producer() { list.push(ssParser(proxy)); } break; - // case 'ssr': - // list.push(ssrParser(proxy)); - // break; + case 'ssr': + if (opts['include-unsupported-proxy']) { + list.push(ssrParser(proxy)); + } else { + throw new Error( + `Platform sing-box does not support proxy type: ${proxy.type}`, + ); + } + break; case 'vmess': if ( !proxy.network || diff --git a/backend/src/restful/download.js b/backend/src/restful/download.js index 8c81663..daa0289 100644 --- a/backend/src/restful/download.js +++ b/backend/src/restful/download.js @@ -20,7 +20,15 @@ async function downloadSubscription(req, res) { req.query.target || getPlatformFromHeaders(req.headers) || 'JSON'; $.info(`正在下载订阅:${name}`); - let { url, ua, content, mergeSources, ignoreFailedRemoteSub } = req.query; + let { + url, + ua, + content, + mergeSources, + ignoreFailedRemoteSub, + produceType, + includeUnsupportedProxy, + } = req.query; if (url) { url = decodeURIComponent(url); $.info(`指定远程订阅 URL: ${url}`); @@ -41,6 +49,14 @@ async function downloadSubscription(req, res) { ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub); $.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`); } + if (produceType) { + produceType = decodeURIComponent(produceType); + $.info(`指定生产类型: ${produceType}`); + } + if (includeUnsupportedProxy) { + includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy); + $.info(`包含不支持的节点: ${includeUnsupportedProxy}`); + } const allSubs = $.read(SUBS_KEY); const sub = findByName(allSubs, name); @@ -55,6 +71,10 @@ async function downloadSubscription(req, res) { content, mergeSources, ignoreFailedRemoteSub, + produceType, + produceOpts: { + 'include-unsupported-proxy': includeUnsupportedProxy, + }, }); if (sub.source !== 'local' || url) { @@ -121,12 +141,22 @@ async function downloadCollection(req, res) { $.info(`正在下载组合订阅:${name}`); - let { ignoreFailedRemoteSub } = req.query; + let { ignoreFailedRemoteSub, produceType, includeUnsupportedProxy } = + req.query; if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') { ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub); $.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`); } + if (produceType) { + produceType = decodeURIComponent(produceType); + $.info(`指定生产类型: ${produceType}`); + } + + if (includeUnsupportedProxy) { + includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy); + $.info(`包含不支持的节点: ${includeUnsupportedProxy}`); + } if (collection) { try { @@ -135,6 +165,10 @@ async function downloadCollection(req, res) { name, platform, ignoreFailedRemoteSub, + produceType, + produceOpts: { + 'include-unsupported-proxy': includeUnsupportedProxy, + }, }); // forward flow header from the first subscription in this collection diff --git a/backend/src/restful/preview.js b/backend/src/restful/preview.js index c9845a0..47bd073 100644 --- a/backend/src/restful/preview.js +++ b/backend/src/restful/preview.js @@ -3,6 +3,7 @@ import { ProxyUtils } from '@/core/proxy-utils'; import { findByName } from '@/utils/database'; import { success, failed } from './response'; import download from '@/utils/download'; +import { render } from '@/utils/tpl'; import { SUBS_KEY } from '@/constants'; import $ from '@/core/app'; @@ -59,10 +60,14 @@ async function previewFile(req, res) { } // parse proxies const files = (Array.isArray(content) ? content : [content]).flat(); - const filesContent = files + let filesContent = files .filter((i) => i != null && i !== '') .join('\n'); + if (file.isTpl) { + filesContent = await render(filesContent); + } + // apply processors const processed = Array.isArray(file.process) && file.process.length > 0 diff --git a/backend/src/restful/sync.js b/backend/src/restful/sync.js index 5e88c35..8bd8772 100644 --- a/backend/src/restful/sync.js +++ b/backend/src/restful/sync.js @@ -13,6 +13,7 @@ import download from '@/utils/download'; import { ProxyUtils } from '@/core/proxy-utils'; import { RuleUtils } from '@/core/rule-utils'; import { syncToGist } from '@/restful/artifacts'; +import { render } from '@/utils/tpl'; export default function register($app) { // Initialization @@ -33,6 +34,8 @@ async function produceArtifact({ mergeSources, ignoreFailedRemoteSub, ignoreFailedRemoteFile, + produceType, + produceOpts = {}, }) { platform = platform || 'JSON'; @@ -154,7 +157,7 @@ async function produceArtifact({ exist[proxy.name] = true; } // produce - return ProxyUtils.produce(proxies, platform); + return ProxyUtils.produce(proxies, platform, produceType, produceOpts); } else if (type === 'collection') { const allSubs = $.read(SUBS_KEY); const allCols = $.read(COLLECTIONS_KEY); @@ -301,7 +304,7 @@ async function produceArtifact({ } exist[proxy.name] = true; } - return ProxyUtils.produce(proxies, platform); + return ProxyUtils.produce(proxies, platform, produceType, produceOpts); } else if (type === 'rule') { const allRules = $.read(RULES_KEY); const rule = findByName(allRules, name); @@ -419,10 +422,13 @@ async function produceArtifact({ } } const files = (Array.isArray(raw) ? raw : [raw]).flat(); - const filesContent = files + let filesContent = files .filter((i) => i != null && i !== '') .join('\n'); + if (file.isTpl) { + filesContent = await render(filesContent); + } // apply processors const processed = Array.isArray(file.process) && file.process.length > 0 diff --git a/backend/src/utils/platform.js b/backend/src/utils/platform.js index 570f2a7..7f8fefd 100644 --- a/backend/src/utils/platform.js +++ b/backend/src/utils/platform.js @@ -20,7 +20,7 @@ export function getPlatformFromHeaders(headers) { } else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) { return 'Loon'; } else if (UA.indexOf('Shadowrocket') !== -1) { - return 'ShadowRocket'; + return 'Shadowrocket'; } else if (UA.indexOf('Stash') !== -1) { return 'Stash'; } else if ( diff --git a/backend/src/utils/tpl.js b/backend/src/utils/tpl.js new file mode 100644 index 0000000..3d55254 --- /dev/null +++ b/backend/src/utils/tpl.js @@ -0,0 +1,210 @@ +import nunjucks from 'nunjucks'; +import { ProxyUtils } from '@/core/proxy-utils'; +import { produceArtifact } from '@/restful/sync'; +import lodash from 'lodash'; +import $ from '@/core/app'; +import scriptResourceCache from '@/utils/script-resource-cache'; +import { getFlowHeaders, parseFlowHeaders, flowTransfer } from '@/utils/flow'; +const flowUtils = { getFlowHeaders, parseFlowHeaders, flowTransfer }; +const n = nunjucks.configure({ autoescape: false }); + +n.addFilter( + 'produceArtifact', + (...args) => { + const callback = args.pop(); + const name = args[0]; + const type = args[1]; + const platform = args[2]; + const produceType = args[3]; + const nameRegex = args[4]; + const nameRegexFlags = args[5]; + produceArtifact({ + type, + name, + platform, + produceType, + }) + .then((artifact) => { + callback( + null, + artifact.filter(({ tag }) => + nameRegex + ? new RegExp(nameRegex, nameRegexFlags).test(tag) + : true, + ), + ); + }) + .catch((e) => { + $.error(`produceArtifact filter error: ${e.message ?? e}`); + callback(e); + }); + }, + true, +); +n.addFilter( + 'subNode', + (...args) => { + const callback = args.pop(); + const name = args[0]; + const nameRegex = args[1]; + const nameRegexFlags = args[2]; + produceArtifact({ + type: 'subscription', + name, + platform: 'sing-box', + produceType: 'internal', + }) + .then((artifact) => { + callback( + null, + JSON.stringify( + artifact.filter(({ tag }) => + nameRegex + ? new RegExp(nameRegex, nameRegexFlags).test( + tag, + ) + : true, + ), + ).replace(/(^\[|\]$)/g, ''), + ); + }) + .catch((e) => { + $.error(`subNode filter error: ${e.message ?? e}`); + callback(e); + }); + }, + true, +); +n.addFilter( + 'colNode', + (...args) => { + const callback = args.pop(); + const name = args[0]; + const nameRegex = args[1]; + const nameRegexFlags = args[2]; + produceArtifact({ + type: 'collection', + name, + platform: 'sing-box', + produceType: 'internal', + }) + .then((artifact) => { + callback( + null, + JSON.stringify( + artifact.filter(({ tag }) => + nameRegex + ? new RegExp(nameRegex, nameRegexFlags).test( + tag, + ) + : true, + ), + ).replace(/(^\[|\]$)/g, ''), + ); + }) + .catch((e) => { + $.error(`colNode filter error: ${e.message ?? e}`); + callback(e); + }); + }, + true, +); +n.addFilter( + 'sub', + (...args) => { + const callback = args.pop(); + const name = args[0]; + const nameRegex = args[1]; + const nameRegexFlags = args[2]; + produceArtifact({ + type: 'subscription', + name, + platform: 'sing-box', + produceType: 'internal', + }) + .then((artifact) => { + callback( + null, + JSON.stringify( + artifact + .filter(({ tag }) => + nameRegex + ? new RegExp( + nameRegex, + nameRegexFlags, + ).test(tag) + : true, + ) + .map((p) => p.tag), + ).replace(/(^\[|\]$)/g, ''), + ); + }) + .catch((e) => { + $.error(`sub filter error: ${e.message ?? e}`); + callback(e); + }); + }, + true, +); +n.addFilter( + 'col', + (...args) => { + const callback = args.pop(); + const name = args[0]; + const nameRegex = args[1]; + const nameRegexFlags = args[2]; + produceArtifact({ + type: 'collection', + name, + platform: 'sing-box', + produceType: 'internal', + }) + .then((artifact) => { + callback( + null, + JSON.stringify( + artifact + .filter(({ tag }) => + nameRegex + ? new RegExp( + nameRegex, + nameRegexFlags, + ).test(tag) + : true, + ) + .map((p) => p.tag), + ).replace(/(^\[|\]$)/g, ''), + ); + }) + .catch((e) => { + $.error(`col filter error: ${e.message ?? e}`); + callback(e); + }); + }, + true, +); + +export const render = async (tpl = '', data = {}) => { + return new Promise((resolve) => { + n.renderString( + tpl, + { + $substore: $, + lodash: lodash, + ProxyUtils: ProxyUtils, + scriptResourceCache: scriptResourceCache, + flowUtils: flowUtils, + // produceArtifact: produceArtifact, + ...data, + }, + (e, result) => { + if (e) { + $.error(`rendering error: ${e.message ?? e}`); + resolve(''); + } else { + resolve(result); + } + }, + ); + }); +};