diff --git a/README.md b/README.md index ade79fff7..cdf56c235 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

-Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket. +Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.

[![Build](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/sub-store-org/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/sub-store-org/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/sub-store-org/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/sub-store-org/Sub-Store) @@ -31,23 +31,25 @@ Core functionalities: - [x] SSD URI - [x] V2RayN URI - [x] Hysteria2 URI -- [x] QX (SS, SSR, VMess, Trojan, HTTP) -- [x] Loon (SS, SSR, VMess, Trojan, HTTP, WireGuard, VLESS, Hysteria2) -- [x] Surge (SS, VMess, Trojan, HTTP, TUIC, Snell, Hysteria2, SSR(external, only for macOS), WireGuard(Surge to Surge)) -- [x] ShadowRocket (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, Hysteria2) -- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria, Hysteria2) -- [x] Stash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria) -- [x] Clash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard) +- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5) +- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, WireGuard, VLESS, Hysteria2) +- [x] Surge (SS, VMess, Trojan, HTTP, SOCKS5, TUIC, Snell, Hysteria2, SSR(external, only for macOS), WireGuard(Surge to Surge)) +- [x] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, WireGuard(Surfboard to Surfboard)) +- [x] Shadowrocket (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria2, TUIC) +- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria2, TUIC) +- [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC) +- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard) ### Supported Target Platforms - [x] QX - [x] Loon - [x] Surge +- [x] Surfboard - [x] Stash - [x] Clash.Meta - [x] Clash -- [x] ShadowRocket +- [x] Shadowrocket - [x] V2Ray - [x] V2Ray URI - [x] Plain JSON diff --git a/backend/package.json b/backend/package.json index e394784da..1a56eb441 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "sub-store", - "version": "2.14.133", + "version": "2.14.134", "description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.", "main": "src/main.js", "scripts": { diff --git a/backend/src/core/proxy-utils/producers/index.js b/backend/src/core/proxy-utils/producers/index.js index 449a0f2ba..0e87ec1d5 100644 --- a/backend/src/core/proxy-utils/producers/index.js +++ b/backend/src/core/proxy-utils/producers/index.js @@ -8,6 +8,7 @@ import URI_Producer from './uri'; import V2Ray_Producer from './v2ray'; import QX_Producer from './qx'; import ShadowRocket_Producer from './shadowrocket'; +import Surfboard_Producer from './surfboard'; function JSON_Producer() { const type = 'ALL'; @@ -27,4 +28,5 @@ export default { JSON: JSON_Producer(), Stash: Stash_Producer(), ShadowRocket: ShadowRocket_Producer(), + Surfboard: Surfboard_Producer(), }; diff --git a/backend/src/core/proxy-utils/producers/surfboard.js b/backend/src/core/proxy-utils/producers/surfboard.js new file mode 100644 index 000000000..5c7658e90 --- /dev/null +++ b/backend/src/core/proxy-utils/producers/surfboard.js @@ -0,0 +1,199 @@ +import { Result, isPresent } from './utils'; +import { isNotBlank } from '@/utils'; +// import $ from '@/core/app'; + +const targetPlatform = 'Surfboard'; + +export default function Surfboard_Producer() { + const produce = (proxy) => { + switch (proxy.type) { + case 'ss': + return shadowsocks(proxy); + case 'trojan': + return trojan(proxy); + case 'vmess': + return vmess(proxy); + case 'http': + return http(proxy); + case 'socks5': + return socks5(proxy); + case 'wireguard-surge': + return wireguard(proxy); + } + throw new Error( + `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`, + ); + }; + return { produce }; +} + +function shadowsocks(proxy) { + const result = new Result(proxy); + result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); + result.append(`,encrypt-method=${proxy.cipher}`); + result.appendIfPresent(`,password=${proxy.password}`, 'password'); + + // obfs + if (isPresent(proxy, 'plugin')) { + if (proxy.plugin === 'obfs') { + result.append(`,obfs=${proxy['plugin-opts'].mode}`); + result.appendIfPresent( + `,obfs-host=${proxy['plugin-opts'].host}`, + 'plugin-opts.host', + ); + result.appendIfPresent( + `,obfs-uri=${proxy['plugin-opts'].path}`, + 'plugin-opts.path', + ); + } else { + throw new Error(`plugin ${proxy.plugin} is not supported`); + } + } + + // udp + result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); + + return result.toString(); +} + +function trojan(proxy) { + const result = new Result(proxy); + result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); + result.appendIfPresent(`,password=${proxy.password}`, 'password'); + + // transport + handleTransport(result, proxy); + + // tls + result.appendIfPresent(`,tls=${proxy.tls}`, 'tls'); + + // tls verification + result.appendIfPresent(`,sni=${proxy.sni}`, 'sni'); + result.appendIfPresent( + `,skip-cert-verify=${proxy['skip-cert-verify']}`, + 'skip-cert-verify', + ); + + // tfo + result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); + + // udp + result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); + + return result.toString(); +} + +function vmess(proxy) { + const result = new Result(proxy); + result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); + result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid'); + + // transport + handleTransport(result, proxy); + + // AEAD + if (isPresent(proxy, 'aead')) { + result.append(`,vmess-aead=${proxy.aead}`); + } else { + result.append(`,vmess-aead=${proxy.alterId === 0}`); + } + + // tls + result.appendIfPresent(`,tls=${proxy.tls}`, 'tls'); + + // tls verification + result.appendIfPresent(`,sni=${proxy.sni}`, 'sni'); + result.appendIfPresent( + `,skip-cert-verify=${proxy['skip-cert-verify']}`, + 'skip-cert-verify', + ); + + // udp + result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); + + return result.toString(); +} + +function http(proxy) { + const result = new Result(proxy); + const type = proxy.tls ? 'https' : 'http'; + result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`); + result.appendIfPresent(`,${proxy.username}`, 'username'); + result.appendIfPresent(`,${proxy.password}`, 'password'); + + // tls verification + result.appendIfPresent(`,sni=${proxy.sni}`, 'sni'); + result.appendIfPresent( + `,skip-cert-verify=${proxy['skip-cert-verify']}`, + 'skip-cert-verify', + ); + + // udp + result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); + + return result.toString(); +} + +function socks5(proxy) { + const result = new Result(proxy); + const type = proxy.tls ? 'socks5-tls' : 'socks5'; + result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`); + result.appendIfPresent(`,${proxy.username}`, 'username'); + result.appendIfPresent(`,${proxy.password}`, 'password'); + + // tls verification + result.appendIfPresent(`,sni=${proxy.sni}`, 'sni'); + result.appendIfPresent( + `,skip-cert-verify=${proxy['skip-cert-verify']}`, + 'skip-cert-verify', + ); + + // udp + result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); + + return result.toString(); +} + +function wireguard(proxy) { + const result = new Result(proxy); + + result.append(`${proxy.name}=wireguard`); + + result.appendIfPresent( + `,section-name=${proxy['section-name']}`, + 'section-name', + ); + + return result.toString(); +} + +function handleTransport(result, proxy) { + if (isPresent(proxy, 'network')) { + if (proxy.network === 'ws') { + result.append(`,ws=true`); + if (isPresent(proxy, 'ws-opts')) { + result.appendIfPresent( + `,ws-path=${proxy['ws-opts'].path}`, + 'ws-opts.path', + ); + if (isPresent(proxy, 'ws-opts.headers')) { + const headers = proxy['ws-opts'].headers; + const value = Object.keys(headers) + .map((k) => { + let v = headers[k]; + if (['Host'].includes(k)) { + v = `"${v}"`; + } + return `${k}:${v}`; + }) + .join('|'); + if (isNotBlank(value)) { + result.append(`,ws-headers=${value}`); + } + } + } + } else { + throw new Error(`network ${proxy.network} is unsupported`); + } + } +} diff --git a/backend/src/core/proxy-utils/producers/surgemac.js b/backend/src/core/proxy-utils/producers/surgemac.js index c3e3cdaa1..ca8a3bb52 100644 --- a/backend/src/core/proxy-utils/producers/surgemac.js +++ b/backend/src/core/proxy-utils/producers/surgemac.js @@ -1,7 +1,7 @@ import { Result } from './utils'; import Surge_Producer from './surge'; -const targetPlatform = 'SurgeMac'; +// const targetPlatform = 'SurgeMac'; const surge_Producer = Surge_Producer(); @@ -10,24 +10,9 @@ export default function SurgeMac_Producer() { switch (proxy.type) { case 'ssr': return shadowsocksr(proxy); - case 'ss': - return surge_Producer.produce(proxy); - case 'trojan': - return surge_Producer.produce(proxy); - case 'vmess': - return surge_Producer.produce(proxy); - case 'http': - return surge_Producer.produce(proxy); - case 'socks5': - return surge_Producer.produce(proxy); - case 'snell': - return surge_Producer.produce(proxy); - case 'tuic': + default: return surge_Producer.produce(proxy); } - throw new Error( - `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`, - ); }; return { produce }; } diff --git a/backend/src/utils/platform.js b/backend/src/utils/platform.js index 70eae4ab9..80f262e0e 100644 --- a/backend/src/utils/platform.js +++ b/backend/src/utils/platform.js @@ -11,6 +11,8 @@ export function getPlatformFromHeaders(headers) { } if (UA.indexOf('Quantumult%20X') !== -1) { return 'QX'; + } else if (UA.indexOf('Surfboard') !== -1) { + return 'Surfboard'; } else if (UA.indexOf('Surge Mac') !== -1) { return 'SurgeMac'; } else if (UA.indexOf('Surge') !== -1) {