Compare commits

...

87 Commits

Author SHA1 Message Date
xream
f1e1d50a2c fix: 暂时将后端上传限制放宽到 1mb 2023-09-01 02:07:24 +08:00
Peng-YM
a65cd1f1c9 Update README.md 2023-08-31 16:41:50 +08:00
xream
5b0e2e1ef2 docs: config 2023-08-30 22:52:05 +08:00
xream
b29266ac57 chore: sync to GitLab 2023-08-30 16:19:17 +08:00
xream
336ddd6706 chore: 调整部分日志 2023-08-29 13:52:02 +08:00
xream
25ec219659 docs: 更新 Surge SSR 协议说明; 模块说明页增加更新说明的链接 2023-08-29 01:59:01 +08:00
xream
41d24b131a feat: 根据 UA 识别 macOS 版 Surge(也可指定参数 target=SurgeMac) 并支持 SSR 协议(节点字段 exec 为 ssr-local 路径, 默认 /usr/local/bin/ssr-local; 端口从 10000 开始递增, 暂不支持配置) 2023-08-29 01:46:49 +08:00
xream
ba78982f41 feat: 统一将 VMess 和 VLESS 的 http 传输层的 path 和 Host 处理为数组 2023-08-28 23:47:10 +08:00
xream
26193301b3 fix: 仅在 VMess/VLESS 且传输层为 http 时设置 Host 为数组 2023-08-28 23:38:03 +08:00
xream
0141e48200 feat: 增加还原备份完成的日志输出 2023-08-28 23:29:53 +08:00
xream
5ae6687b1f chore: changelog 2023-08-28 23:15:48 +08:00
xream
ad6d1ab441 fix: build dist 2023-08-28 20:41:40 +08:00
xream
f5aea14904 fix: 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名(不覆盖现有的 Host) 2023-08-28 20:34:22 +08:00
Peng-YM
4f2c95f6ab chore: remove unnecessary files 2023-08-28 20:07:32 +08:00
Peng-YM
be1e2c9979 chore: removed tracking dist files from git 2023-08-28 20:06:50 +08:00
Peng-YM
347b19e30d remove: deprecated artifact 2023-08-28 20:01:05 +08:00
xream
f94a12bf6e feat: bundle 2023-08-28 19:01:34 +08:00
xream
bd510a9aa9 fix: sync 2023-08-28 18:48:33 +08:00
xream
f02af9d643 fix: vless servername 2023-08-28 15:32:08 +08:00
xream
af8e965866 feat: new target platform "Clash.Meta" 2023-08-28 13:10:48 +08:00
xream
4bebcff1d3 feat: 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名 2023-08-28 00:09:24 +08:00
xream
7b8f6f7e52 feat: 域名解析新增 Tencent, Ali; 脚本下载失败, 脚本操作失败, 脚本过滤失败时都会报错了 2023-08-27 23:17:57 +08:00
xream
49c7107d20 fix: transport headers may have no Host 2023-08-27 18:17:30 +08:00
xream
8bfa4dbbf2 feat: VLESS URI 2023-08-27 00:57:21 +08:00
xream
5e14d05c30 feat: 组合订阅错误信息将包含出现错误的子订阅名称; 获取流量失败时, 不影响节点订阅; 订阅上游无有效节点时将报错 2023-08-26 20:27:12 +08:00
xream
8c5dca71fb feat: Loon WireGuard 2023-08-26 15:00:46 +08:00
xream
4973454f58 feat: wireguard 2023-08-25 22:48:03 +08:00
xream
4c6ba2cdc8 feat: hysteria 2023-08-25 16:19:08 +08:00
Hsiaoyi
9cbbd0e86f Merge pull request #233 from eltociear/master-1
Fix typo in README.md
2023-08-24 21:46:03 +08:00
xream
0320a77451 feat: producers adjustments, VMess URI formats 2023-08-24 21:43:58 +08:00
xream
afb9296158 feat: Added support for VMess URI in other formats and VMess without transport settings 2023-08-24 20:23:48 +08:00
xream
9b0c15ebc2 fix: 兼容 value 为空的 Trojan URI 2023-08-24 11:38:27 +08:00
xream
46738d5947 fix: trojan network tcp 2023-08-24 11:08:43 +08:00
xream
1f505752ae fix: trojan uri and tls 2023-08-24 10:02:03 +08:00
Ikko Eltociear Ashimine
0734a3d563 Fix typo in README.md
Speicial -> Special
2023-08-24 00:46:24 +09:00
xream
497bc264e3 fix: servername/sni priority over wss host 2023-08-22 18:21:34 +08:00
xream
feb207b333 fix: servername/sni priority over wss host 2023-08-22 17:28:39 +08:00
xream
9ac1112b37 fix: VMess URI alterId parseInt 2023-08-22 15:29:55 +08:00
xream
96769598ef fix: QX tls 2023-08-22 00:42:53 +08:00
xream
f8ed6a3342 fix: QX tls 2023-08-22 00:08:53 +08:00
xream
99b19c410d fix: vmess/vless http-opts.path/http-opts.headers.Host must be an array in some clients 2023-08-21 22:16:07 +08:00
xream
9e54507bbb fix: double quotes in Surge vmess ws-headers Host 2023-08-21 21:20:31 +08:00
xream
20afa0ad22 Surge 默认模块不带 ability 参数; 分离出固定带参和不带参的模块 2023-08-20 17:22:51 +08:00
walkxspace
c5b6960b35 Update geo.js (#231) 2023-08-19 11:44:55 +08:00
xream
4dd86cb368 feat: Added replaceArtifact API 2023-08-18 13:48:37 +08:00
xream
4a0319e95f fix: flexible cipher for Loon 2023-08-15 21:22:33 +08:00
xream
090d8a978f feat: Added support for scy of VMESS URI 2023-08-15 18:15:04 +08:00
xream
bc9fae6062 feat: Added support for SNI & allowInsecure of Trojan URI 2023-08-15 17:25:25 +08:00
xream
048344268c feat: Added replaceSubscriptions, replaceCollection API 2023-08-15 15:48:57 +08:00
xream
c5746f6a6b Fixed: fast-open tfo 2023-08-15 14:59:27 +08:00
xream
5cb226da62 feat: Added support for SS URI in other formats 2023-08-15 01:48:54 +08:00
xream
d229047744 Fixed: unsupported cipher for Clash/Stash 2023-08-14 10:04:47 +08:00
Hsiaoyi
cb21a8e6ec Merge pull request #229 from xream/feature/tuic
Adjust the logic for determining the tuic version
2023-08-13 17:03:40 +08:00
xream
537a00e8a9 Adjust the logic for determining the tuic version 2023-08-13 17:00:44 +08:00
Hsiaoyi
b770578cba Merge pull request #228 from xream/feature/tuic
feat: Added support for tuic and some compatibility adjustments
2023-08-13 15:56:52 +08:00
xream
47a95e5a3d feat: Added support for tuic and some compatibility adjustments 2023-08-13 15:54:04 +08:00
Hsiaoyi
e99f13d487 Merge pull request #227 from xream/feature/snell
feat: Added support for producing snell nodes with reuse and optional obfs
2023-07-31 18:44:53 +08:00
xream
fcab8401e0 feat: Added support for producing snell nodes with reuse and optional obfs 2023-07-31 18:41:48 +08:00
Hsiaoyi
431b1a3c8e Merge pull request #226 from Keywos/master
fixed deleted gist
2023-07-31 17:42:49 +08:00
Hsiaoyi
36d46003d6 Fixed: empty uploading files 2023-07-31 17:42:31 +08:00
K
ff71f12996 version 2.14.3 2023-07-31 16:41:22 +08:00
K
f7c08e3a56 fixed deleted gist 2023-07-31 16:38:07 +08:00
K
6eea8bb2d0 Merge branch 'sub-store-org:master' into master 2023-07-31 14:58:54 +08:00
Hsiaoyi
fc90e22a48 Added Surge-Noability.sgmodule 2023-07-28 22:38:36 +08:00
Hsiaoyi
26d47b019b Merge pull request #223 from xream/feature/V2Ray
feat: V2Ray Producer
Fixes #180
2023-07-26 09:55:45 +08:00
xream
8e49a78f45 feat: V2Ray Producer 2023-07-26 09:48:14 +08:00
Hsiaoyi
edee10cee3 Update Surge.sgmodule 2023-07-26 09:03:59 +08:00
K
20d958d74f Update Surge.sgmodule 2023-07-26 01:48:47 +08:00
Hsiaoyi
6427f99545 Update Surge.sgmodule
ability=http-client-policy
2023-07-24 14:39:21 +08:00
Hsiaoyi
7d2ea10206 Merge pull request #219 from Keywos/script-Cache
surge
2023-07-23 18:00:42 +08:00
Hsiaoyi
e862235cb8 Merge pull request #220 from xream/fix/FullConfig
fix: Full Config Preprocessor
2023-07-23 17:42:06 +08:00
xream
38f1728e42 fix: Full Config Preprocessor 2023-07-23 17:38:29 +08:00
K
d963be87f8 [!] Surge 2023-07-23 15:11:32 +08:00
K
390e4540d2 Merge branch 'script-Cache' of https://github.com/Keywos/Sub-Store into script-Cache 2023-07-22 15:24:20 +08:00
K
0bd00406f3 [-] log 2023-07-22 15:24:19 +08:00
Hsiaoyi
b9ce4e8f20 Merge pull request #218 from sub-store-org/dependabot/npm_and_yarn/backend/axios-0.21.2
build(deps-dev): bump axios from 0.20.0 to 0.21.2 in /backend
2023-07-22 14:35:52 +08:00
Hsiaoyi
de15bbf3ea using Node.js v16 2023-07-22 14:34:06 +08:00
dependabot[bot]
5d6bd1415b build(deps-dev): bump axios from 0.20.0 to 0.21.2 in /backend
Bumps [axios](https://github.com/axios/axios) from 0.20.0 to 0.21.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.21.2/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.20.0...v0.21.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-22 06:17:45 +00:00
Hsiaoyi
6e9c3ead4c Merge pull request #217 from Keywos/script-Cache
[+] version 2.14.0
2023-07-22 14:15:56 +08:00
K
b3ccd5743a Merge branch 'sub-store-org:master' into script-Cache 2023-07-22 14:13:09 +08:00
K
e18c215fe4 [+] version 2023-07-22 14:11:45 +08:00
Hsiaoyi
e4b54b43a1 Merge pull request #216 from Keywos/script-Cache
script-Cache
2023-07-22 13:56:54 +08:00
K
21726bf950 script-Cache 2023-07-22 13:53:47 +08:00
Hsiaoyi
f6ca9af00f fix: tasks cache in Node.js environment (#209) 2023-05-09 17:16:35 +08:00
Hsiaoyi
39b79b6ca4 feat: Added support for producing Surge nodes with test-url (#199) 2023-03-19 18:32:34 +08:00
NobyDa
999271fa9d Improve resource cache key. (#190) 2023-02-08 12:30:26 +08:00
QuentinHsu
5de35c7720 🐞 fix(subscriptions): negative usage flow (#175) 2022-10-25 00:07:23 +08:00
55 changed files with 6022 additions and 4567 deletions

View File

@@ -1,15 +1,15 @@
name: build
on:
on:
push:
branches:
- master
paths:
- 'backend/package.json'
- "backend/package.json"
pull_request:
branches:
- master
paths:
- 'backend/package.json'
- "backend/package.json"
jobs:
build:
runs-on: ubuntu-latest
@@ -17,11 +17,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
with:
ref: 'master'
ref: "master"
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: "14"
node-version: "16"
- name: Install dependencies
run: |
npm install -g pnpm
@@ -34,17 +34,28 @@ jobs:
run: |
cd backend
pnpm run build
- name: Bundle
run: |
cd backend
pnpm i -D estrella
pnpm run bundle
- id: tag
name: Generate release tag
run: |
cd backend
SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"`
echo "::set-output name=release_tag::$SUBSTORE_RELEASE"
- name: Prepare release
run: |
cd backend
pnpm i -D conventional-changelog-cli
pnpm run changelog
- name: Release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
body_path: ./backend/CHANGELOG.md
tag_name: ${{ steps.tag.outputs.release_tag }}
files: |
./backend/sub-store.min.js
@@ -52,3 +63,9 @@ jobs:
./backend/dist/sub-store-1.min.js
./backend/dist/sub-store-parser.loon.min.js
./backend/dist/cron-sync-artifacts.min.js
./backend/dist/sub-store.bundle.js
- name: Sync to GitLab
env:
GITLAB_PIPELINE_TOKEN: ${{ secrets.GITLAB_PIPELINE_TOKEN }}
run: |
curl -X POST --fail -F token=$GITLAB_PIPELINE_TOKEN -F ref=master https://gitlab.com/api/v4/projects/48891296/trigger/pipeline

9
.gitignore vendored
View File

@@ -127,4 +127,11 @@ out
*.ntvs*
*.njsproj
*.sln
*.sw?
*.sw?
# Dist files
backend/dist/*
!backend/dist/.gitkeep
backend/sub-store.min.js
CHANGELOG.md

View File

@@ -10,8 +10,8 @@
Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.
</p>
[![Build](https://github.com/Peng-YM/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/Peng-YM/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/Peng-YM/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/Peng-YM/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/Peng-YM/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/Peng-YM/Sub-Store)
[![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)
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/PengYM)
Core functionalities:
@@ -19,6 +19,8 @@ Core functionalities:
1. Conversion among various formats.
2. Subscription formatting.
3. Collect multiple subscriptions in one URL.
> The following descriptions of features may not be updated in real-time. Please refer to the actual available features for accurate information.
## 1. Subscription Conversion
@@ -29,17 +31,25 @@ Core functionalities:
- [x] SSD URI
- [x] V2RayN URI
- [x] QX (SS, SSR, VMess, Trojan, HTTP)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP)
- [x] Surge (SS, VMess, Trojan, HTTP)
- [x] Stash & Clash (SS, SSR, VMess, Trojan, HTTP)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, WireGuard, VLESS)
- [x] Surge (SS, VMess, Trojan, HTTP, TUIC, Snell, SSR(external, only for macOS))
- [x] ShadowRocket (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria)
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria)
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria)
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, Snell)
### Supported Target Platforms
- [x] QX
- [x] Loon
- [x] Surge
- [x] Stash & Clash
- [x] Stash
- [x] Clash.Meta
- [x] Clash
- [x] ShadowRocket
- [x] V2Ray
- [x] V2Ray URI
- [x] Plain JSON
## 2. Subscription Formatting
@@ -61,6 +71,7 @@ Core functionalities:
- [x] **Regex rename operator**: replace by regex in proxy names.
- [x] **Regex delete operator**: delete by regex in proxy names.
- [x] **Script operator**: modify proxy by script.
- [x] **Resolve Domain Operator**: resolve the domain of nodes to an IP address.
### Development
@@ -88,7 +99,12 @@ This project is under the GPL V3 LICENSE.
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store?ref=badge_large)
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=sub-store-org/sub-store&type=Date)](https://star-history.com/#sub-store-org/sub-store&Date)
## Acknowledgements
- Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work!
- Speicial thanks to @Orz-3 and @58xinian for their awesome icons.
- Special thanks to @Orz-3 and @58xinian for their awesome icons.

View File

@@ -9,7 +9,7 @@
* @updated: <%= updated %>
* @version: <%= pkg.version %>
* @author: Peng-YM
* @github: https://github.com/Peng-YM/Sub-Store
* @github: https://github.com/sub-store-org/Sub-Store
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
*/

23
backend/bundle.js Normal file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
let content = fs.readFileSync(path.join(__dirname, 'sub-store.min.js'), {
encoding: 'utf8',
});
content = content.replace(
/eval\(('|")(require\(('|").*?('|")\))('|")\)/g,
'$2',
);
fs.writeFileSync(path.join(__dirname, 'dist/sub-store.no-bundle.js'), content, {
encoding: 'utf8',
});
const { build } = require('estrella');
build({
entry: 'dist/sub-store.no-bundle.js',
outfile: 'dist/sub-store.bundle.js',
bundle: true,
platform: 'node',
});

0
backend/dist/.gitkeep vendored Normal file
View File

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.13.4",
"version": "2.14.46",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js",
"scripts": {
@@ -8,7 +8,9 @@
"test": "gulp peggy && npx cross-env BABEL_ENV=test mocha src/test/**/*.spec.js --require @babel/register --recursive",
"serve": "node sub-store.min.js",
"start": "nodemon -w src -w package.json --exec babel-node src/main.js",
"build": "gulp"
"build": "gulp",
"bundle": "node bundle.js",
"changelog": "conventional-changelog -p cli -i CHANGELOG.md -s"
},
"author": "Peng-YM",
"license": "GPL-3.0",
@@ -30,7 +32,7 @@
"@babel/preset-env": "^7.18.0",
"@babel/register": "^7.17.7",
"@types/gulp": "^4.0.9",
"axios": "^0.20.0",
"axios": "^0.21.2",
"babel-plugin-relative-path-import": "^2.0.1",
"babelify": "^10.0.0",
"browser-pack-flat": "^3.4.2",
@@ -51,4 +53,4 @@
"prettier-plugin-sort-imports": "^1.6.1",
"tinyify": "^3.0.0"
}
}
}

8528
backend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,3 +9,5 @@ export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';
export const RESOURCE_CACHE_KEY = '#sub-store-cached-resource';
export const CACHE_EXPIRATION_TIME_MS = 60 * 60 * 1000; // 1 hour
export const SCRIPT_RESOURCE_CACHE_KEY = '#sub-store-cached-script-resource'; // cached-script-resource CSR
export const CSR_EXPIRATION_TIME_KEY = '#sub-store-csr-expiration-time'; // Custom expiration time key; (Loon|Surge) Default write 48 hour

View File

@@ -1,5 +1,5 @@
import download from '@/utils/download';
import { isIPv4, isIPv6 } from '@/utils';
import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
import PROXY_PREPROCESSORS from './preprocessors';
import PROXY_PRODUCERS from './producers';
@@ -36,7 +36,7 @@ function parse(raw) {
if (lastParser) {
const [proxy, error] = tryParse(lastParser, line);
if (!error) {
proxies.push(proxy);
proxies.push(lastParse(proxy));
success = true;
}
}
@@ -46,7 +46,7 @@ function parse(raw) {
for (const parser of PROXY_PARSERS) {
const [proxy, error] = tryParse(parser, line);
if (!error) {
proxies.push(proxy);
proxies.push(lastParse(proxy));
lastParser = parser;
success = true;
$.info(`${parser.name} is activated`);
@@ -90,8 +90,7 @@ async function process(proxies, operators = [], targetPlatform) {
$.error(
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`,
);
// skip the script if download failed.
continue;
throw new Error(`无法下载脚本: ${url}`);
}
} else {
script = content;
@@ -137,10 +136,21 @@ function produce(proxies, targetPlatform) {
$.info(`Producing proxies for target: ${targetPlatform}`);
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
let localPort = 10000;
return proxies
.map((proxy) => {
try {
return producer.produce(proxy);
let line = producer.produce(proxy);
if (
line.length > 0 &&
line.includes('__SubStoreLocalPort__')
) {
line = line.replace(
/__SubStoreLocalPort__/g,
localPort++,
);
}
return line;
} catch (err) {
$.error(
`Cannot produce proxy: ${JSON.stringify(
@@ -182,3 +192,59 @@ function safeMatch(parser, line) {
return false;
}
}
function lastParse(proxy) {
if (proxy.type === 'trojan') {
if (proxy.network === 'tcp') {
delete proxy.network;
}
}
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) {
proxy.tls = true;
}
if (proxy.tls && !proxy.sni) {
if (proxy.network) {
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
transportHost = Array.isArray(transportHost)
? transportHost[0]
: transportHost;
if (transportHost) {
proxy.sni = transportHost;
}
}
if (!proxy.sni && !isIP(proxy.server)) {
proxy.sni = proxy.server;
}
}
// 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名(不覆盖现有的 Host)
if (
!proxy.tls &&
['ws', 'http'].includes(proxy.network) &&
!proxy[`${proxy.network}-opts`]?.headers?.Host &&
!isIP(proxy.server)
) {
proxy[`${proxy.network}-opts`] = proxy[`${proxy.network}-opts`] || {};
proxy[`${proxy.network}-opts`].headers =
proxy[`${proxy.network}-opts`].headers || {};
proxy[`${proxy.network}-opts`].headers.Host =
['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http'
? [proxy.server]
: proxy.server;
}
// 统一将 VMess 和 VLESS 的 http 传输层的 path 和 Host 处理为数组
if (['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http') {
let transportPath = proxy[`${proxy.network}-opts`]?.path;
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
if (transportHost && !Array.isArray(transportHost)) {
proxy[`${proxy.network}-opts`].headers.Host = [transportHost];
}
if (transportPath && !Array.isArray(transportPath)) {
proxy[`${proxy.network}-opts`].path = [transportPath];
}
}
return proxy;
}
function isIP(ip) {
return isIPv4(ip) || isIPv6(ip);
}

View File

@@ -23,12 +23,19 @@ function URI_SS() {
};
content = content.split('#')[0]; // strip proxy name
// handle IPV4 and IPV6
const serverAndPort = content.match(/@([^/]*)(\/|$)/)[1];
let serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
let userInfoStr = Base64.decode(content.split('@')[0]);
if (!serverAndPortArray) {
content = Base64.decode(content);
userInfoStr = content.split('@')[0];
serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
}
const serverAndPort = serverAndPortArray[1];
const portIdx = serverAndPort.lastIndexOf(':');
proxy.server = serverAndPort.substring(0, portIdx);
proxy.port = serverAndPort.substring(portIdx + 1);
const userInfo = Base64.decode(content.split('@')[0]).split(':');
const userInfo = userInfoStr.split(':');
proxy.cipher = userInfo[0];
proxy.password = userInfo[1];
@@ -150,7 +157,7 @@ function URI_VMess() {
};
const parse = (line) => {
line = line.split('vmess://')[1];
const content = Base64.decode(line);
let content = Base64.decode(line);
if (/=\s*vmess/.test(content)) {
// Quantumult VMess URI format
const partitions = content.split(',').map((p) => p.trim());
@@ -202,30 +209,94 @@ function URI_VMess() {
}
return proxy;
} else {
// V2rayN URI format
const params = JSON.parse(content);
let params = {};
try {
// V2rayN URI format
params = JSON.parse(content);
} catch (e) {
// Shadowrocket URI format
// eslint-disable-next-line no-unused-vars
let [__, base64Line, qs] = /(^[^?]+?)\/?\?(.*)$/.exec(line);
content = Base64.decode(base64Line);
for (const addon of qs.split('&')) {
const [key, valueRaw] = addon.split('=');
let value = valueRaw;
value = decodeURIComponent(valueRaw);
if (value.indexOf(',') === -1) {
params[key] = value;
} else {
params[key] = value.split(',');
}
}
// eslint-disable-next-line no-unused-vars
let [___, cipher, uuid, server, port] =
/(^[^:]+?):([^:]+?)@(.*):(\d+)$/.exec(content);
params.scy = cipher;
params.id = uuid;
params.port = port;
params.add = server;
}
const proxy = {
name: params.ps,
name: params.ps ?? params.remark,
type: 'vmess',
server: params.add,
port: params.port,
cipher: 'auto', // V2rayN has no default cipher! use aes-128-gcm as default.
port: parseInt(getIfPresent(params.port), 10),
cipher: getIfPresent(params.scy, 'auto'),
uuid: params.id,
alterId: getIfPresent(params.aid, 0),
tls: params.tls === 'tls' || params.tls === true,
alterId: parseInt(
getIfPresent(params.aid ?? params.alterId, 0),
10,
),
tls: ['tls', true, 1, '1'].includes(params.tls),
'skip-cert-verify': isPresent(params.verify_cert)
? !params.verify_cert
: undefined,
};
// https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
if (proxy.tls && proxy.sni) {
proxy.sni = params.sni;
}
// handle obfs
if (params.net === 'ws') {
if (params.net === 'ws' || params.obfs === 'websocket') {
proxy.network = 'ws';
proxy['ws-opts'] = {
path: getIfNotBlank(params.path),
headers: { Host: getIfNotBlank(params.host) },
};
if (proxy.tls && params.host) {
proxy.sni = params.host;
} else if (
['tcp', 'http'].includes(params.net) ||
params.obfs === 'http'
) {
proxy.network = 'http';
}
if (proxy.network) {
let transportHost = params.host ?? params.obfsParam;
let transportPath = params.path;
if (proxy.network === 'http') {
if (transportHost) {
transportHost = Array.isArray(transportHost)
? transportHost[0]
: transportHost;
}
if (transportPath) {
transportPath = Array.isArray(transportPath)
? transportPath[0]
: transportPath;
}
}
if (transportPath || transportHost) {
proxy[`${proxy.network}-opts`] = {
path: getIfNotBlank(transportPath),
headers: { Host: getIfNotBlank(transportHost) },
};
} else {
delete proxy.network;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L413
// sni 优先级应高于 host
if (proxy.tls && !proxy.sni && transportHost) {
proxy.sni = transportHost;
}
}
return proxy;
@@ -234,6 +305,85 @@ function URI_VMess() {
return { name, test, parse };
}
function URI_VLESS() {
const name = 'URI VLESS Parser';
const test = (line) => {
return /^vless:\/\//.test(line);
};
const parse = (line) => {
line = line.split('vless://')[1];
// eslint-disable-next-line no-unused-vars
let [__, uuid, server, port, addons, name] =
/^(.*?)@(.*?):(\d+)\/?\?(.*?)(?:#(.*?))$/.exec(line);
port = parseInt(`${port}`, 10);
uuid = decodeURIComponent(uuid);
name = decodeURIComponent(name) ?? `VLESS ${server}:${port}`;
const proxy = {
type: 'vless',
name,
server,
port,
uuid,
};
const params = {};
for (const addon of addons.split('&')) {
const [key, valueRaw] = addon.split('=');
let value = valueRaw;
value = decodeURIComponent(valueRaw);
params[key] = value;
}
proxy.tls = params.security && params.security !== 'none';
proxy.sni = params.sni;
proxy.flow = params.flow;
proxy['client-fingerprint'] = params.fp;
proxy.alpn = params.alpn ? params.alpn.split(',') : undefined;
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure);
if (['reality'].includes(params.security)) {
const opts = {};
if (params.pbk) {
opts['public-key'] = params.pbk;
}
if (params.sid) {
opts['short-id'] = params.sid;
}
if (Object.keys(opts).length > 0) {
proxy[`${params.security}-opts`] = opts;
}
}
proxy.network = params.type;
if (proxy.network && !['tcp', 'none'].includes(proxy.network)) {
const opts = {};
if (params.path) {
opts.path = params.path;
}
if (params.host) {
opts.headers = { Host: params.host };
}
if (params.serviceName) {
opts[`${proxy.network}-service-name`] = params.serviceName;
}
if (Object.keys(opts).length > 0) {
proxy[`${proxy.network}-opts`] = opts;
}
}
if (proxy.tls && !proxy.sni) {
if (proxy.network === 'ws') {
proxy.sni = proxy['ws-opts']?.headers?.Host;
} else if (proxy.network === 'http') {
let httpHost = proxy['http-opts']?.headers?.Host;
proxy.sni = Array.isArray(httpHost) ? httpHost[0] : httpHost;
}
}
return proxy;
};
return { name, test, parse };
}
// Trojan URI format
function URI_Trojan() {
const name = 'URI Trojan Parser';
@@ -270,6 +420,10 @@ function Clash_All() {
'http',
'snell',
'trojan',
'tuic',
'vless',
'hysteria',
'wireguard',
].includes(proxy.type)
) {
throw new Error(
@@ -278,9 +432,19 @@ function Clash_All() {
}
// handle vmess sni
if (proxy.type === 'vmess') {
if (['vmess', 'vless'].includes(proxy.type)) {
proxy.sni = proxy.servername;
delete proxy.servername;
if (proxy.tls && !proxy.sni) {
if (proxy.network === 'ws') {
proxy.sni = proxy['ws-opts']?.headers?.Host;
} else if (proxy.network === 'http') {
let httpHost = proxy['http-opts']?.headers?.Host;
proxy.sni = Array.isArray(httpHost)
? httpHost[0]
: httpHost;
}
}
}
return proxy;
@@ -417,6 +581,113 @@ function Loon_Http() {
return { name, test, parse };
}
function Loon_WireGuard() {
const name = 'Loon WireGuard Parser';
const test = (line) => {
return /^.*=\s*wireguard/i.test(line.split(',')[0]);
};
const parse = (line) => {
const name = line.match(
/(^.*?)\s*?=\s*?wireguard\s*?,.+?\s*?=\s*?.+?/i,
)?.[1];
line = line.replace(name, '').replace(/^\s*?=\s*?wireguard\s*/i, '');
let peers = line.match(
/,\s*?peers\s*?=\s*?\[\s*?\{\s*?(.+?)\s*?\}\s*?\]/i,
)?.[1];
let serverPort = peers.match(
/(,|^)\s*?endpoint\s*?=\s*?"?(.+?):(\d+)"?\s*?(,|$)/i,
);
let server = serverPort?.[2];
let port = parseInt(serverPort?.[3], 10);
let mtu = line.match(/(,|^)\s*?mtu\s*?=\s*?"?(\d+?)"?\s*?(,|$)/i)?.[2];
if (mtu) {
mtu = parseInt(mtu, 10);
}
let keepalive = line.match(
/(,|^)\s*?keepalive\s*?=\s*?"?(\d+?)"?\s*?(,|$)/i,
)?.[2];
if (keepalive) {
keepalive = parseInt(keepalive, 10);
}
let reserved = peers.match(
/(,|^)\s*?reserved\s*?=\s*?"?(\[\s*?.+?\s*?\])"?\s*?(,|$)/i,
)?.[2];
if (reserved) {
reserved = JSON.parse(reserved);
}
let dns;
let dnsv4 = line.match(/(,|^)\s*?dns\s*?=\s*?"?(.+?)"?\s*?(,|$)/i)?.[2];
let dnsv6 = line.match(
/(,|^)\s*?dnsv6\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
if (dnsv4 || dnsv6) {
dns = [];
if (dnsv4) {
dns.push(dnsv4);
}
if (dnsv6) {
dns.push(dnsv6);
}
}
let allowedIps = peers
.match(/(,|^)\s*?allowed-ips\s*?=\s*?"(.+?)"\s*?(,|$)/i)?.[2]
?.split(',')
.map((i) => i.trim());
let preSharedKey = peers.match(
/(,|^)\s*?preshared-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
let ip = line.match(
/(,|^)\s*?interface-ip\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
let ipv6 = line.match(
/(,|^)\s*?interface-ipv6\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
let publicKey = peers.match(
/(,|^)\s*?public-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
const proxy = {
type: 'wireguard',
name,
server,
port,
ip,
ipv6,
'private-key': line.match(
/(,|^)\s*?private-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2],
'public-key': publicKey,
mtu,
keepalive,
reserved,
'allowed-ips': allowedIps,
'preshared-key': preSharedKey,
dns,
udp: true,
peers: [
{
server,
port,
ip,
ipv6,
'public-key': publicKey,
'pre-shared-key': preSharedKey,
allowed_ips: allowedIps,
reserved,
},
],
};
proxy;
if (Array.isArray(proxy.dns) && proxy.dns.length > 0) {
proxy['remote-dns-resolve'] = true;
}
return proxy;
};
return { name, test, parse };
}
function Surge_SS() {
const name = 'Surge SS Parser';
const test = (line) => {
@@ -474,10 +745,20 @@ function Surge_Snell() {
return { name, test, parse };
}
function Surge_Tuic() {
const name = 'Surge Tuic Parser';
const test = (line) => {
return /^.*=\s*tuic(-v5)??/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
export default [
URI_SS(),
URI_SSR(),
URI_VMess(),
URI_VLESS(),
URI_Trojan(),
Clash_All(),
Surge_SS(),
@@ -485,6 +766,7 @@ export default [
Surge_Trojan(),
Surge_Http(),
Surge_Snell(),
Surge_Tuic(),
Surge_Socks5(),
Loon_SS(),
Loon_SSR(),
@@ -492,6 +774,7 @@ export default [
Loon_Vless(),
Loon_Trojan(),
Loon_Http(),
Loon_WireGuard(),
QX_SS(),
QX_SSR(),
QX_VMess(),

View File

@@ -25,11 +25,14 @@ const grammars = String.raw`
proxy.network = "ws";
$set(proxy, "ws-opts.path", obfs.path);
$set(proxy, "ws-opts.headers", obfs['ws-headers']);
if (proxy['ws-opts'] && proxy['ws-opts']['headers'] && proxy['ws-opts']['headers'].Host) {
proxy['ws-opts']['headers'].Host = proxy['ws-opts']['headers'].Host.replace(/^"(.*)"$/, '$1')
}
}
}
}
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls) {
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5) {
return proxy;
}
@@ -73,6 +76,13 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
$set(proxy, "obfs-opts.path", obfs.path);
}
}
tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/others)* {
proxy.type = "tuic";
}
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/others)* {
proxy.type = "tuic";
proxy.version = 5;
}
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* {
proxy.type = "socks5";
}
@@ -175,6 +185,11 @@ uri = $[^,]+
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _

View File

@@ -23,11 +23,14 @@
proxy.network = "ws";
$set(proxy, "ws-opts.path", obfs.path);
$set(proxy, "ws-opts.headers", obfs['ws-headers']);
if (proxy['ws-opts'] && proxy['ws-opts']['headers'] && proxy['ws-opts']['headers'].Host) {
proxy['ws-opts']['headers'].Host = proxy['ws-opts']['headers'].Host.replace(/^"(.*)"$/, '$1')
}
}
}
}
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls) {
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5) {
return proxy;
}
@@ -71,6 +74,13 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
$set(proxy, "obfs-opts.path", obfs.path);
}
}
tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/others)* {
proxy.type = "tuic";
}
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/others)* {
proxy.type = "tuic";
proxy.version = 5;
}
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* {
proxy.type = "socks5";
}
@@ -173,6 +183,11 @@ uri = $[^,]+
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _

View File

@@ -79,7 +79,7 @@ port = digits:[0-9]+ {
}
}
params = "?" head:param tail:("&"@param)* {
params = "/"? "?" head:param tail:("&"@param)* {
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
proxy.sni = params["sni"] || params["peer"];
@@ -87,6 +87,16 @@ params = "?" head:param tail:("&"@param)* {
proxy.network = "ws";
$set(proxy, "ws-opts.path", params["wspath"]);
}
if (params["type"]) {
proxy.network = params["type"]
if (params["path"]) {
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
}
if (params["host"]) {
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
}
}
proxy.udp = toBool(params["udp"]);
proxy.tfo = toBool(params["tfo"]);
@@ -94,7 +104,7 @@ params = "?" head:param tail:("&"@param)* {
param = kv/single;
kv = key:$[a-z]i+ "=" value:$[^&#]i+ {
kv = key:$[a-z]i+ "=" value:$[^&#]i* {
params[key] = value;
}

View File

@@ -77,7 +77,7 @@ port = digits:[0-9]+ {
}
}
params = "?" head:param tail:("&"@param)* {
params = "/"? "?" head:param tail:("&"@param)* {
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
proxy.sni = params["sni"] || params["peer"];
@@ -85,6 +85,16 @@ params = "?" head:param tail:("&"@param)* {
proxy.network = "ws";
$set(proxy, "ws-opts.path", params["wspath"]);
}
if (params["type"]) {
proxy.network = params["type"]
if (params["path"]) {
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
}
if (params["host"]) {
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
}
}
proxy.udp = toBool(params["udp"]);
proxy.tfo = toBool(params["tfo"]);
@@ -92,7 +102,7 @@ params = "?" head:param tail:("&"@param)* {
param = kv/single;
kv = key:$[a-z]i+ "=" value:$[^&#]i+ {
kv = key:$[a-z]i+ "=" value:$[^&#]i* {
params[key] = value;
}

View File

@@ -95,22 +95,10 @@ function FullConfig() {
return /^(\[server_local\]|\[Proxy\])/gm.test(raw);
};
const parse = function (raw) {
const regex = /^\[server_local]|\[Proxy]/gm;
const match = regex.exec(raw);
const results = [];
let first = true;
if (match) {
raw = raw.substring(match.index);
for (const line of raw.split('\n')) {
if (!first && !line.test(/^\s*\[/)) {
results.push(line);
}
// skip the first line
first = false;
}
return results.join('\n');
}
const match = raw.match(
/^\[server_local|Proxy\]([\s\S]+?)^\[.+?\](\r?\n|$)/im,
)?.[1];
return match || raw;
};
return { name, test, parse };
}

View File

@@ -1,4 +1,5 @@
import resourceCache from '@/utils/resource-cache';
import scriptResourceCache from '@/utils/script-resource-cache';
import { isIPv4, isIPv6 } from '@/utils';
import { FULL } from '@/utils/logical';
import { getFlag } from '@/utils/geo';
@@ -377,6 +378,46 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result);
return result;
},
Ali: async function (domain) {
const id = hex_md5(`ALI:${domain}`);
const cached = resourceCache.get(id);
if (cached) return cached;
const resp = await $.http.get({
url: `http://223.6.6.6/resolve?name=${encodeURIComponent(
domain,
)}&type=A&short=1`,
headers: {
accept: 'application/dns-json',
},
});
const answers = JSON.parse(resp.body);
if (answers.length === 0) {
throw new Error('No answers');
}
const result = answers[answers.length - 1];
resourceCache.set(id, result);
return result;
},
Tencent: async function (domain) {
const id = hex_md5(`ALI:${domain}`);
const cached = resourceCache.get(id);
if (cached) return cached;
const resp = await $.http.get({
url: `http://119.28.28.28/d?type=A&dn=${encodeURIComponent(
domain,
)}`,
headers: {
accept: 'application/dns-json',
},
});
const answers = resp.body.split(';').map((i) => i.split(',')[0]);
if (answers.length === 0) {
throw new Error('No answers');
}
const result = answers[answers.length - 1];
resourceCache.set(id, result);
return result;
},
};
function ResolveDomainOperator({ provider }) {
@@ -565,7 +606,8 @@ async function ApplyFilter(filter, objs) {
selected = await filter.func(objs);
} catch (err) {
// print log and skip this filter
$.log(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
$.error(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
throw new Error(`脚本过滤失败 ${err.message ?? err}`);
}
return objs.filter((_, i) => selected[i]);
}
@@ -577,7 +619,8 @@ async function ApplyOperator(operator, objs) {
if (output_) output = output_;
} catch (err) {
// print log and skip this operator
$.log(`Cannot apply operator ${operator.name}! Reason: ${err}`);
$.error(`Cannot apply operator ${operator.name}! Reason: ${err}`);
throw new Error(`脚本操作失败 ${err.message ?? err}`);
}
return output;
}
@@ -633,6 +676,7 @@ function createDynamicFunction(name, script, $arguments) {
'$httpClient',
'$notification',
'ProxyUtils',
'scriptResourceCache',
`${script}\n return ${name}`,
)(
$arguments,
@@ -645,6 +689,7 @@ function createDynamicFunction(name, script, $arguments) {
// eslint-disable-next-line no-undef
$notification,
ProxyUtils,
scriptResourceCache,
);
} else {
return new Function(
@@ -652,7 +697,8 @@ function createDynamicFunction(name, script, $arguments) {
'$substore',
'lodash',
'ProxyUtils',
'scriptResourceCache',
`${script}\n return ${name}`,
)($arguments, $, lodash, ProxyUtils);
)($arguments, $, lodash, ProxyUtils, scriptResourceCache);
}
}

View File

@@ -4,11 +4,28 @@ export default function Clash_Producer() {
const type = 'ALL';
const produce = (proxies) => {
// filter unsupported proxies
proxies = proxies.filter((proxy) =>
['ss', 'ssr', 'vmess', 'socks', 'http', 'snell', 'trojan'].includes(
proxy.type,
),
);
proxies = proxies.filter((proxy) => {
if (
![
'ss',
'ssr',
'vmess',
'socks',
'http',
'snell',
'trojan',
'wireguard',
].includes(proxy.type)
) {
return false;
} else if (
proxy.type === 'snell' &&
String(proxy.version) === '4'
) {
return false;
}
return true;
});
return (
'proxies:\n' +
proxies
@@ -25,8 +42,54 @@ export default function Clash_Producer() {
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 (
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];
}
}
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) {
delete proxy.tls;
}
delete proxy['tls-fingerprint'];
return ' - ' + JSON.stringify(proxy) + '\n';
})

View File

@@ -0,0 +1,117 @@
import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function ClashMeta_Producer() {
const type = 'ALL';
const produce = (proxies) => {
return (
'proxies:\n' +
proxies
.filter((proxy) => {
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 (
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') {
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'];
}
if (
['vmess', 'vless'].includes(proxy.type) &&
proxy.network === 'http'
) {
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];
}
}
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) {
delete proxy.tls;
}
delete proxy['tls-fingerprint'];
return ' - ' + JSON.stringify(proxy) + '\n';
})
.join('')
);
};
return { type, produce };
}

View File

@@ -1,9 +1,13 @@
import Surge_Producer from './surge';
import SurgeMac_Producer from './surgemac';
import Clash_Producer from './clash';
import ClashMeta_Producer from './clashmeta';
import Stash_Producer from './stash';
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';
function JSON_Producer() {
const type = 'ALL';
@@ -14,9 +18,13 @@ function JSON_Producer() {
export default {
QX: QX_Producer(),
Surge: Surge_Producer(),
SurgeMac: SurgeMac_Producer(),
Loon: Loon_Producer(),
Clash: Clash_Producer(),
ClashMeta: ClashMeta_Producer(),
URI: URI_Producer(),
V2Ray: V2Ray_Producer(),
JSON: JSON_Producer(),
Stash: Stash_Producer(),
ShadowRocket: ShadowRocket_Producer(),
};

View File

@@ -1,6 +1,7 @@
/* eslint-disable no-case-declarations */
const targetPlatform = 'Loon';
import { isPresent, Result } from './utils';
import { isIPv4, isIPv6 } from '@/utils';
export default function Loon_Producer() {
const produce = (proxy) => {
@@ -17,6 +18,8 @@ export default function Loon_Producer() {
return vless(proxy);
case 'http':
return http(proxy);
case 'wireguard':
return wireguard(proxy);
}
throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
@@ -98,7 +101,7 @@ function trojan(proxy) {
'ws-opts.path',
);
result.appendIfPresent(
`,host=${proxy['ws-opts'].headers.Host}`,
`,host=${proxy['ws-opts']?.headers?.Host}`,
'ws-opts.headers.Host',
);
} else {
@@ -127,9 +130,7 @@ function trojan(proxy) {
function vmess(proxy) {
const result = new Result(proxy);
result.append(
`${proxy.name}=vmess,${proxy.server},${proxy.port},${
proxy.cipher === 'auto' ? 'none' : proxy.cipher
},"${proxy.uuid}"`,
`${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.uuid}"`,
);
// transport
@@ -137,21 +138,23 @@ function vmess(proxy) {
if (proxy.network === 'ws') {
result.append(`,transport=ws`);
result.appendIfPresent(
`,path=${proxy['ws-opts'].path}`,
`,path=${proxy['ws-opts']?.path}`,
'ws-opts.path',
);
result.appendIfPresent(
`,host=${proxy['ws-opts'].headers.Host}`,
`,host=${proxy['ws-opts']?.headers?.Host}`,
'ws-opts.headers.Host',
);
} else if (proxy.network === 'http') {
result.append(`,transport=http`);
let httpPath = proxy['http-opts']?.path;
let httpHost = proxy['http-opts']?.headers?.Host;
result.appendIfPresent(
`,path=${proxy['http-opts'].path}`,
`,path=${Array.isArray(httpPath) ? httpPath[0] : httpPath}`,
'http-opts.path',
);
result.appendIfPresent(
`,host=${proxy['http-opts'].headers.Host}`,
`,host=${Array.isArray(httpHost) ? httpHost[0] : httpHost}`,
'http-opts.headers.Host',
);
} else {
@@ -189,6 +192,9 @@ function vmess(proxy) {
}
function vless(proxy) {
if (proxy['reality-opts']) {
throw new Error(`reality is unsupported`);
}
const result = new Result(proxy);
result.append(
`${proxy.name}=vless,${proxy.server},${proxy.port},"${proxy.uuid}"`,
@@ -199,21 +205,23 @@ function vless(proxy) {
if (proxy.network === 'ws') {
result.append(`,transport=ws`);
result.appendIfPresent(
`,path=${proxy['ws-opts'].path}`,
`,path=${proxy['ws-opts']?.path}`,
'ws-opts.path',
);
result.appendIfPresent(
`,host=${proxy['ws-opts'].headers.Host}`,
`,host=${proxy['ws-opts']?.headers?.Host}`,
'ws-opts.headers.Host',
);
} else if (proxy.network === 'http') {
result.append(`,transport=http`);
let httpPath = proxy['http-opts']?.path;
let httpHost = proxy['http-opts']?.headers?.Host;
result.appendIfPresent(
`,path=${proxy['http-opts'].path}`,
`,path=${Array.isArray(httpPath) ? httpPath[0] : httpPath}`,
'http-opts.path',
);
result.appendIfPresent(
`,host=${proxy['http-opts'].headers.Host}`,
`,host=${Array.isArray(httpHost) ? httpHost[0] : httpHost}`,
'http-opts.headers.Host',
);
} else {
@@ -266,3 +274,63 @@ function http(proxy) {
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
return result.toString();
}
function wireguard(proxy) {
if (Array.isArray(proxy.peers) && proxy.peers.length > 0) {
proxy.server = proxy.peers[0].server;
proxy.port = proxy.peers[0].port;
proxy.ip = proxy.peers[0].ip;
proxy.ipv6 = proxy.peers[0].ipv6;
proxy['public-key'] = proxy.peers[0]['public-key'];
proxy['preshared-key'] = proxy.peers[0]['pre-shared-key'];
proxy['allowed-ips'] = proxy.peers[0]['allowed_ips'];
proxy.reserved = proxy.peers[0].reserved;
}
const result = new Result(proxy);
result.append(`${proxy.name}=wireguard`);
result.appendIfPresent(`,interface-ip=${proxy.ip}`, 'ip');
result.appendIfPresent(`,interface-ipv6=${proxy.ipv6}`, 'ipv6');
result.appendIfPresent(
`,private-key="${proxy['private-key']}"`,
'private-key',
);
result.appendIfPresent(`,mtu=${proxy.mtu}`, 'mtu');
if (proxy.dns) {
if (Array.isArray(proxy.dns)) {
proxy.dnsv6 = proxy.dns.find((i) => isIPv6(i));
proxy.dns = proxy.dns.find((i) => isIPv4(i));
}
}
result.appendIfPresent(`,dns=${proxy.dns}`, 'dns');
result.appendIfPresent(`,dnsv6=${proxy.dnsv6}`, 'dnsv6');
result.appendIfPresent(
`,keepalive=${proxy['persistent-keepalive']}`,
'persistent-keepalive',
);
result.appendIfPresent(`,keepalive=${proxy.keepalive}`, 'keepalive');
const allowedIps = Array.isArray(proxy['allowed-ips'])
? proxy['allowed-ips'].join(',')
: proxy['allowed-ips'];
let reserved = Array.isArray(proxy.reserved)
? proxy.reserved.join(',')
: proxy.reserved;
if (reserved) {
reserved = `,reserved=[${reserved}]`;
}
let presharedKey = proxy['preshared-key'] ?? proxy['pre-shared-key'];
if (presharedKey) {
presharedKey = `,preshared-key="${presharedKey}"`;
}
result.append(
`,peers=[{public-key="${proxy['public-key']}",allowed-ips="${
allowedIps ?? '0.0.0.0/0,::/0'
}",endpoint=${proxy.server}:${proxy.port}${reserved ?? ''}${
presharedKey ?? ''
}}]`,
);
return result.toString();
}

View File

@@ -62,18 +62,20 @@ function shadowsocks(proxy) {
);
}
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
if (needTls(proxy)) {
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
}
// tfo
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
@@ -133,11 +135,11 @@ function trojan(proxy) {
if (needTls(proxy)) append(`,obfs=wss`);
else append(`,obfs=ws`);
appendIfPresent(
`,obfs-uri=${proxy['ws-opts'].path}`,
`,obfs-uri=${proxy['ws-opts']?.path}`,
'ws-opts.path',
);
appendIfPresent(
`,obfs-host=${proxy['ws-opts'].headers.Host}`,
`,obfs-host=${proxy['ws-opts']?.headers?.Host}`,
'ws-opts.headers.Host',
);
} else {
@@ -150,18 +152,20 @@ function trojan(proxy) {
append(`,over-tls=true`);
}
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
if (needTls(proxy)) {
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
}
// tfo
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
@@ -206,12 +210,18 @@ function vmess(proxy) {
} else {
throw new Error(`network ${proxy.network} is unsupported`);
}
let transportPath = proxy[`${proxy.network}-opts`]?.path;
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
appendIfPresent(
`,obfs-uri=${proxy[`${proxy.network}-opts`].path}`,
`,obfs-uri=${
Array.isArray(transportPath) ? transportPath[0] : transportPath
}`,
`${proxy.network}-opts.path`,
);
appendIfPresent(
`,obfs-host=${proxy[`${proxy.network}-opts`].headers.Host}`,
`,obfs-host=${
Array.isArray(transportHost) ? transportHost[0] : transportHost
}`,
`${proxy.network}-opts.headers.Host`,
);
} else {
@@ -219,18 +229,20 @@ function vmess(proxy) {
if (proxy.tls) append(`,obfs=over-tls`);
}
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
if (needTls(proxy)) {
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
}
// AEAD
if (isPresent(proxy, 'aead')) {
@@ -266,18 +278,20 @@ function http(proxy) {
}
appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
if (needTls(proxy)) {
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
}
// tfo
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
@@ -306,18 +320,20 @@ function socks5(proxy) {
}
appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
if (needTls(proxy)) {
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
}
// tfo
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
@@ -332,11 +348,5 @@ function socks5(proxy) {
}
function needTls(proxy) {
return (
proxy.tls ||
proxy.sni ||
typeof proxy['skip-cert-verify'] !== 'undefined' ||
typeof proxy['tls-fingerprint'] !== 'undefined' ||
typeof proxy['tls-host'] !== 'undefined'
);
return proxy.tls;
}

View File

@@ -0,0 +1,122 @@
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) => {
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 (
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') {
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 (
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];
}
}
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) {
delete proxy.tls;
}
delete proxy['tls-fingerprint'];
return ' - ' + JSON.stringify(proxy) + '\n';
})
.join('')
);
};
return { type, produce };
}

View File

@@ -6,6 +6,28 @@ export default function Stash_Producer() {
return (
'proxies:\n' +
proxies
.filter((proxy) => {
if (
![
'ss',
'ssr',
'vmess',
'socks',
'http',
'snell',
'trojan',
'tuic',
'vless',
'wireguard',
].includes(proxy.type) ||
(proxy.type === 'snell' &&
String(proxy.version) === '4') ||
(proxy.type === 'vless' && proxy['reality-opts'])
) {
return false;
}
return true;
})
.map((proxy) => {
if (proxy.type === 'vmess') {
// handle vmess aead
@@ -19,8 +41,88 @@ export default function Stash_Producer() {
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') {
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 (
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];
}
}
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) {
delete proxy.tls;
}
delete proxy['tls-fingerprint'];
return ' - ' + JSON.stringify(proxy) + '\n';
})

View File

@@ -4,6 +4,14 @@ import $ from '@/core/app';
const targetPlatform = 'Surge';
const ipVersions = {
dual: 'dual',
ipv4: 'v4-only',
ipv6: 'v6-only',
'ipv4-prefer': 'prefer-v4',
'ipv6-prefer': 'prefer-v6',
};
export default function Surge_Producer() {
const produce = (proxy) => {
switch (proxy.type) {
@@ -19,6 +27,8 @@ export default function Surge_Producer() {
return socks5(proxy);
case 'snell':
return snell(proxy);
case 'tuic':
return tuic(proxy);
}
throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
@@ -55,6 +65,10 @@ function shadowsocks(proxy) {
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
return result.toString();
}
@@ -87,6 +101,10 @@ function trojan(proxy) {
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
return result.toString();
}
@@ -127,6 +145,9 @@ function vmess(proxy) {
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
return result.toString();
}
@@ -155,6 +176,10 @@ function http(proxy) {
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
return result.toString();
}
@@ -185,6 +210,10 @@ function socks5(proxy) {
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
return result.toString();
}
@@ -196,20 +225,67 @@ function snell(proxy) {
// obfs
result.appendIfPresent(
`,obfs=${proxy['obfs-opts'].mode}`,
`,obfs=${proxy['obfs-opts']?.mode}`,
'obfs-opts.mode',
);
result.appendIfPresent(
`,obfs-host=${proxy['obfs-opts'].host}`,
`,obfs-host=${proxy['obfs-opts']?.host}`,
'obfs-opts.host',
);
result.appendIfPresent(
`,obfs-uri=${proxy['obfs-opts'].path}`,
`,obfs-uri=${proxy['obfs-opts']?.path}`,
'obfs-opts.path',
);
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// reuse
result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse');
return result.toString();
}
function tuic(proxy) {
const result = new Result(proxy);
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
let type = proxy.type;
if (!proxy.token || proxy.token.length === 0) {
type = 'tuic-v5';
}
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,uuid=${proxy.uuid}`, 'uuid');
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(`,token=${proxy.token}`, 'token');
result.appendIfPresent(
`,alpn=${Array.isArray(proxy.alpn) ? proxy.alpn[0] : proxy.alpn}`,
'alpn',
);
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
// 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['fast-open']}`, 'fast-open');
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
return result.toString();
}
@@ -225,7 +301,13 @@ function handleTransport(result, proxy) {
if (isPresent(proxy, 'ws-opts.headers')) {
const headers = proxy['ws-opts'].headers;
const value = Object.keys(headers)
.map((k) => `${k}:${headers[k]}`)
.map((k) => {
let v = headers[k];
if (['Host'].includes(k)) {
v = `"${v}"`;
}
return `${k}:${v}`;
})
.join('|');
if (isNotBlank(value)) {
result.append(`,ws-headers=${value}`);

View File

@@ -0,0 +1,65 @@
import { Result } from './utils';
import Surge_Producer from './surge';
const targetPlatform = 'SurgeMac';
const surge_Producer = Surge_Producer();
export default function SurgeMac_Producer() {
const produce = (proxy) => {
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':
return surge_Producer.produce(proxy);
}
throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
);
};
return { produce };
}
function shadowsocksr(proxy) {
const result = new Result(proxy);
proxy.local_port = '__SubStoreLocalPort__';
proxy.local_address = proxy.local_address ?? '127.0.0.1';
result.append(
`${proxy.name} = external, exec = "${
proxy.exec || '/usr/local/bin/ssr-local'
}", address = "${proxy.server}", local-port = ${proxy.local_port}`,
);
for (const [key, value] of Object.entries({
cipher: '-m',
obfs: '-o',
password: '-k',
port: '-p',
protocol: '-O',
'protocol-param': '-G',
server: '-s',
local_port: '-l',
local_address: '-b',
})) {
result.appendIfPresent(
`, args = "${value}", args = "${proxy[key]}"`,
key,
);
}
return result.toString();
}

View File

@@ -55,27 +55,156 @@ export default function URI_Producer() {
break;
case 'vmess':
// V2RayN URI format
let type = '';
let net = proxy.network || 'tcp';
if (proxy.network === 'http') {
net = 'tcp';
type = 'http';
}
result = {
v: '2',
ps: proxy.name,
add: proxy.server,
port: proxy.port,
id: proxy.uuid,
type: '',
type,
aid: 0,
net: proxy.network || 'tcp',
net,
tls: proxy.tls ? 'tls' : '',
};
if (proxy.tls && proxy.sni) {
result.sni = proxy.sni;
}
// obfs
if (proxy.network === 'ws') {
result.path = proxy['ws-opts'].path || '/';
result.host = proxy['ws-opts'].headers.Host || proxy.server;
if (proxy.network) {
let vmessTransportPath =
proxy[`${proxy.network}-opts`]?.path;
let vmessTransportHost =
proxy[`${proxy.network}-opts`]?.headers?.Host;
if (vmessTransportPath) {
result.path = Array.isArray(vmessTransportPath)
? vmessTransportPath[0]
: vmessTransportPath;
}
if (vmessTransportHost) {
result.host = Array.isArray(vmessTransportHost)
? vmessTransportHost[0]
: vmessTransportHost;
}
}
result = 'vmess://' + Base64.encode(JSON.stringify(result));
break;
case 'vless':
let security = 'none';
const isReality = proxy['reality-opts'];
let sid = '';
let pbk = '';
if (isReality) {
security = 'reality';
const publicKey = proxy['reality-opts']?.['public-key'];
if (publicKey) {
pbk = `&pbk=${encodeURIComponent(publicKey)}`;
}
const shortId = proxy['reality-opts']?.['short-id'];
if (shortId) {
sid = `&sid=${encodeURIComponent(shortId)}`;
}
} else if (proxy.tls) {
security = 'tls';
}
let alpn = '';
if (proxy.alpn) {
alpn = `&alpn=${encodeURIComponent(
Array.isArray(proxy.alpn)
? proxy.alpn
: proxy.alpn.join(','),
)}`;
}
let allowInsecure = '';
if (proxy['skip-cert-verify']) {
allowInsecure = `&allowInsecure=1`;
}
let sni = '';
if (proxy.sni) {
sni = `&sni=${encodeURIComponent(proxy.sni)}`;
}
let fp = '';
if (proxy['client-fingerprint']) {
fp = `&fp=${encodeURIComponent(
proxy['client-fingerprint'],
)}`;
}
let flow = '';
if (proxy.flow) {
flow = `&flow=${encodeURIComponent(proxy.flow)}`;
}
let vlessTransport = `&type=${encodeURIComponent(
proxy.network,
)}`;
let vlessTransportServiceName =
proxy[`${proxy.network}-opts`]?.[
`${proxy.network}-service-name`
];
let vlessTransportPath = proxy[`${proxy.network}-opts`]?.path;
let vlessTransportHost =
proxy[`${proxy.network}-opts`]?.headers?.Host;
if (vlessTransportPath) {
vlessTransport += `&path=${encodeURIComponent(
Array.isArray(vlessTransportPath)
? vlessTransportPath[0]
: vlessTransportPath,
)}`;
}
if (vlessTransportHost) {
vlessTransport += `&host=${encodeURIComponent(
Array.isArray(vlessTransportHost)
? vlessTransportHost[0]
: vlessTransportHost,
)}`;
}
if (vlessTransportServiceName) {
vlessTransport += `&serviceName=${encodeURIComponent(
vlessTransportServiceName,
)}`;
}
result = `vless://${proxy.uuid}@${proxy.server}:${
proxy.port
}?${vlessTransport}&security=${encodeURIComponent(
security,
)}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${pbk}#${encodeURIComponent(
proxy.name,
)}`;
break;
case 'trojan':
let trojanTransport = '';
if (proxy.network) {
trojanTransport = `&type=${proxy.network}`;
let trojanTransportPath =
proxy[`${proxy.network}-opts`]?.path;
let trojanTransportHost =
proxy[`${proxy.network}-opts`]?.headers?.Host;
if (trojanTransportPath) {
trojanTransport += `&path=${encodeURIComponent(
Array.isArray(trojanTransportPath)
? trojanTransportPath[0]
: trojanTransportPath,
)}`;
}
if (trojanTransportHost) {
trojanTransport += `&host=${encodeURIComponent(
Array.isArray(trojanTransportHost)
? trojanTransportHost[0]
: trojanTransportHost,
)}`;
}
}
result = `trojan://${proxy.password}@${proxy.server}:${
proxy.port
}#${encodeURIComponent(proxy.name)}`;
}?sni=${encodeURIComponent(proxy.sni || proxy.server)}${
proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''
}${trojanTransport}#${encodeURIComponent(proxy.name)}`;
break;
}
return result;

View File

@@ -0,0 +1,12 @@
/* eslint-disable no-case-declarations */
import { Base64 } from 'js-base64';
import URI_Producer from './uri';
const URI = URI_Producer();
export default function V2Ray_Producer() {
const type = 'ALL';
const produce = (proxies) =>
Base64.encode(proxies.map((proxy) => URI.produce(proxy)).join('\n'));
return { type, produce };
}

View File

@@ -47,7 +47,7 @@ function AllRuleParser() {
}
if (!matched) throw new Error('Invalid rule type: ' + rawType);
} catch (e) {
console.error(`Failed to parse line: ${line}\n Reason: ${e}`);
console.log(`Failed to parse line: ${line}\n Reason: ${e}`);
}
}
return result;

View File

@@ -7,7 +7,7 @@
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
* Advanced Subscription Manager for QX, Loon, Surge and Clash.
* @author: Peng-YM
* @github: https://github.com/Peng-YM/Sub-Store
* @github: https://github.com/sub-store-org/Sub-Store
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
*/
import { version } from '../package.json';

View File

@@ -19,7 +19,10 @@ export default function register($app) {
if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);
// RESTful APIs
$app.route('/api/artifacts').get(getAllArtifacts).post(createArtifact);
$app.route('/api/artifacts')
.get(getAllArtifacts)
.post(createArtifact)
.put(replaceArtifact);
$app.route('/api/artifact/:name')
.get(getArtifact)
@@ -32,6 +35,12 @@ function getAllArtifacts(req, res) {
success(res, allArtifacts);
}
function replaceArtifact(req, res) {
const allArtifacts = req.body;
$.write(allArtifacts, ARTIFACTS_KEY);
success(res);
}
async function getArtifact(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
@@ -131,7 +140,12 @@ async function deleteArtifact(req, res) {
files[encodeURIComponent(artifact.name)] = {
content: '',
};
await syncToGist(files);
// 当别的Sub 删了同步订阅 或 gist里面删了 当前设备没有删除 时 无法删除的bug
try {
await syncToGist(files);
} catch (i) {
$.error(`Function syncToGist: ${name} : ${i}`);
}
}
// delete local cache
deleteByName(allArtifacts, name);

View File

@@ -14,7 +14,8 @@ export default function register($app) {
$app.route('/api/collections')
.get(getAllCollections)
.post(createCollection);
.post(createCollection)
.put(replaceCollection);
}
// collection API
@@ -111,3 +112,9 @@ function getAllCollections(req, res) {
const allCols = $.read(COLLECTIONS_KEY);
success(res, allCols);
}
function replaceCollection(req, res) {
const allCols = req.body;
$.write(allCols, COLLECTIONS_KEY);
success(res);
}

View File

@@ -32,10 +32,18 @@ async function downloadSubscription(req, res) {
});
if (sub.source !== 'local') {
// forward flow headers
const flowInfo = await getFlowHeaders(sub.url);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
try {
// forward flow headers
const flowInfo = await getFlowHeaders(sub.url);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
} catch (err) {
$.error(
`订阅 ${name} 获取流量信息时发生错误: ${JSON.stringify(
err,
)}`,
);
}
}
@@ -50,15 +58,15 @@ async function downloadSubscription(req, res) {
$.notify(
`🌍 Sub-Store 下载订阅失败`,
`❌ 无法下载订阅:${name}`,
`🤔 原因:${JSON.stringify(err)}`,
`🤔 原因:${err.message ?? err}`,
);
$.error(JSON.stringify(err));
$.error(err.message ?? err);
failed(
res,
new InternalServerError(
'INTERNAL_SERVER_ERROR',
`Failed to download subscription: ${name}`,
`Reason: ${JSON.stringify(err)}`,
`Reason: ${err.message ?? err}`,
),
);
}
@@ -101,9 +109,17 @@ async function downloadCollection(req, res) {
if (subnames.length > 0) {
const sub = findByName(allSubs, subnames[0]);
if (sub.source !== 'local') {
const flowInfo = await getFlowHeaders(sub.url);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
try {
const flowInfo = await getFlowHeaders(sub.url);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
} catch (err) {
$.error(
`组合订阅 ${name} 中的子订阅 ${
sub.name
} 获取流量信息时发生错误: ${err.message ?? err}`,
);
}
}
}
@@ -126,7 +142,7 @@ async function downloadCollection(req, res) {
new InternalServerError(
'INTERNAL_SERVER_ERROR',
`Failed to download collection: ${name}`,
`Reason: ${JSON.stringify(err)}`,
`Reason: ${err.message ?? err}`,
),
);
}

View File

@@ -125,8 +125,10 @@ async function gistBackup(req, res) {
$.cache = content;
$.persistCache();
}
// perform migration after restoring from gist
$.info(`perform migration after restoring from gist...`);
migrate();
$.info(`migration completed`);
$.info(`还原备份完成`);
break;
}
success(res);
@@ -136,7 +138,7 @@ async function gistBackup(req, res) {
new InternalServerError(
'BACKUP_FAILED',
`Failed to ${action} data to gist!`,
`Reason: ${JSON.stringify(err)}`,
`Reason: ${err.message ?? err}`,
),
);
}

View File

@@ -44,8 +44,12 @@ export async function updateGitHubAvatar() {
.then((resp) => JSON.parse(resp.body));
settings.avatarUrl = data['avatar_url'];
$.write(settings, SETTINGS_KEY);
} catch (e) {
$.error('Failed to fetch GitHub avatar for User: ' + username);
} catch (err) {
$.error(
`Failed to fetch GitHub avatar for User: ${username}. Reason: ${
err.message ?? err
}`,
);
}
}
}
@@ -67,7 +71,11 @@ export async function updateArtifactStore() {
$.write(settings, SETTINGS_KEY);
}
} catch (err) {
$.error('Failed to fetch artifact store for User: ' + githubUser);
$.error(
`Failed to fetch artifact store for User: ${githubUser}. Reason: ${
err.message ?? err
}`,
);
}
}
}

View File

@@ -20,7 +20,10 @@ export default function register($app) {
.patch(updateSubscription)
.delete(deleteSubscription);
$app.route('/api/subs').get(getAllSubscriptions).post(createSubscription);
$app.route('/api/subs')
.get(getAllSubscriptions)
.post(createSubscription)
.put(replaceSubscriptions);
}
// subscriptions API
@@ -66,8 +69,12 @@ async function getFlowInfo(req, res) {
}
// unit is KB
const upload = Number(flowHeaders.match(/upload=(\d+)/)[1]);
const download = Number(flowHeaders.match(/download=(\d+)/)[1]);
const uploadMatch = flowHeaders.match(/upload=(-?)(\d+)/);
const upload = Number(uploadMatch[1] + uploadMatch[2]);
const downloadMatch = flowHeaders.match(/download=(-?)(\d+)/);
const download = Number(downloadMatch[1] + downloadMatch[2]);
const total = Number(flowHeaders.match(/total=(\d+)/)[1]);
// optional expire timestamp
@@ -198,3 +205,9 @@ function getAllSubscriptions(req, res) {
const allSubs = $.read(SUBS_KEY);
success(res, allSubs);
}
function replaceSubscriptions(req, res) {
const allSubs = req.body;
$.write(allSubs, SUBS_KEY);
success(res);
}

View File

@@ -25,9 +25,6 @@ export default function register($app) {
async function produceArtifact({ type, name, platform }) {
platform = platform || 'JSON';
// produce Clash node format for ShadowRocket
if (platform === 'ShadowRocket') platform = 'Clash';
if (type === 'subscription') {
const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
@@ -45,6 +42,9 @@ async function produceArtifact({ type, name, platform }) {
sub.process || [],
platform,
);
if (proxies.length === 0) {
throw new Error(`订阅 ${name} 中不含有效节点`);
}
// check duplicate
const exist = {};
for (const proxy of proxies) {
@@ -70,6 +70,7 @@ async function produceArtifact({ type, name, platform }) {
const collection = findByName(allCols, name);
const subnames = collection.subscriptions;
const results = {};
const errors = {};
let processed = 0;
await Promise.all(
@@ -100,6 +101,7 @@ async function produceArtifact({ type, name, platform }) {
);
} catch (err) {
processed++;
errors[name] = err;
$.error(
`❌ 处理组合订阅中的子订阅: ${
sub.name
@@ -111,10 +113,18 @@ async function produceArtifact({ type, name, platform }) {
}),
);
if (Object.keys(errors).length > 0) {
throw new Error(
`组合订阅 ${name} 中的子订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
// merge proxies with the original order
let proxies = Array.prototype.concat.apply(
[],
subnames.map((name) => results[name]),
subnames.map((name) => results[name] || []),
);
// apply own processors
@@ -124,7 +134,7 @@ async function produceArtifact({ type, name, platform }) {
platform,
);
if (proxies.length === 0) {
throw new Error(`组合订阅中不含有效节点`);
throw new Error(`组合订阅 ${name} 中不含有效节点`);
}
// check duplicate
const exist = {};
@@ -245,20 +255,20 @@ async function syncArtifact(req, res) {
return;
}
const output = await produceArtifact({
type: artifact.type,
name: artifact.source,
platform: artifact.platform,
});
$.info(
`正在上传配置:${artifact.name}\n>>>${JSON.stringify(
artifact,
null,
2,
)}`,
);
try {
const output = await produceArtifact({
type: artifact.type,
name: artifact.source,
platform: artifact.platform,
});
$.info(
`正在上传配置:${artifact.name}\n>>>${JSON.stringify(
artifact,
null,
2,
)}`,
);
const resp = await syncToGist({
[encodeURIComponent(artifact.name)]: {
content: output,

View File

@@ -1,13 +1,14 @@
import { HTTP } from '@/vendor/open-api';
import { HTTP, ENV } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5';
import resourceCache from '@/utils/resource-cache';
const tasks = new Map();
export default async function download(url, ua) {
const { isNode } = ENV();
ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)';
const id = hex_md5(ua + url);
if (tasks.has(id)) {
if (!isNode && tasks.has(id)) {
return tasks.get(id);
}
@@ -39,6 +40,8 @@ export default async function download(url, ua) {
}
});
tasks.set(id, result);
if (!isNode) {
tasks.set(id, result);
}
return result;
}

View File

@@ -155,7 +155,7 @@ export function getFlag(name) {
'🇲🇴': ['Macao', '澳门', '澳門', 'CTM'],
'🇲🇹': ['Malta', '马耳他'],
'🇲🇽': ['Mexico', '墨西哥'],
'🇲🇾': ['Malaysia', '马来西亚', '馬來西亞', '吉隆坡', '大馬'],
'🇲🇾': ['Malaysia', '马来', '馬來', '吉隆坡', '大馬'],
'🇳🇱': ['Netherlands', '荷兰', '荷蘭', '尼德蘭', '阿姆斯特丹'],
'🇳🇴': ['Norway', '挪威'],
'🇳🇵': ['Nepal', '尼泊尔'],

View File

@@ -40,6 +40,10 @@ export default class Gist {
}
async upload(files) {
if (Object.keys(files).length === 0) {
return Promise.reject('未提供需上传的文件');
}
const id = await this.locate();
if (id === -1) {

View File

@@ -1,14 +1,18 @@
export function getPlatformFromHeaders(headers) {
const keys = Object.keys(headers);
let UA = '';
let ua = '';
for (let k of keys) {
if (/USER-AGENT/i.test(k)) {
UA = headers[k];
ua = UA.toLowerCase();
break;
}
}
if (UA.indexOf('Quantumult%20X') !== -1) {
return 'QX';
} else if (UA.indexOf('Surge Mac') !== -1) {
return 'SurgeMac';
} else if (UA.indexOf('Surge') !== -1) {
return 'Surge';
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
@@ -17,6 +21,15 @@ export function getPlatformFromHeaders(headers) {
return 'ShadowRocket';
} else if (UA.indexOf('Stash') !== -1) {
return 'Stash';
} else if (
ua === 'meta' ||
(ua.indexOf('clash') !== -1 && ua.indexOf('meta') !== -1)
) {
return 'ClashMeta';
} else if (ua.indexOf('clash') !== -1) {
return 'Clash';
} else if (ua.indexOf('v2ray') !== -1) {
return 'V2Ray';
} else {
return 'JSON';
}

View File

@@ -16,8 +16,13 @@ class ResourceCache {
let clear = false;
Object.entries(this.resourceCache).forEach((entry) => {
const [id, updated] = entry;
if (new Date().getTime() - updated > this.expires) {
if (!updated.time) {
// clear old version cache
delete this.resourceCache[id];
$.delete(`#${id}`);
clear = true;
}
if (new Date().getTime() - updated.time > this.expires) {
delete this.resourceCache[id];
clear = true;
}
@@ -26,9 +31,6 @@ class ResourceCache {
}
revokeAll() {
Object.keys(this.resourceCache).forEach((id) => {
$.delete(`#${id}`);
});
this.resourceCache = {};
this._persist();
}
@@ -38,17 +40,16 @@ class ResourceCache {
}
get(id) {
const updated = this.resourceCache[id];
const updated = this.resourceCache[id] && this.resourceCache[id].time;
if (updated && new Date().getTime() - updated <= this.expires) {
return $.read(`#${id}`);
return this.resourceCache[id].data;
}
return null;
}
set(id, value) {
this.resourceCache[id] = new Date().getTime();
this.resourceCache[id] = { time: new Date().getTime(), data: value };
this._persist();
$.write(value, `#${id}`);
}
}

View File

@@ -0,0 +1,105 @@
import $ from '@/core/app';
import {
SCRIPT_RESOURCE_CACHE_KEY,
CSR_EXPIRATION_TIME_KEY,
} from '@/constants';
class ResourceCache {
constructor() {
this.expires = getExpiredTime();
if (!$.read(SCRIPT_RESOURCE_CACHE_KEY)) {
$.write('{}', SCRIPT_RESOURCE_CACHE_KEY);
}
this.resourceCache = JSON.parse($.read(SCRIPT_RESOURCE_CACHE_KEY));
this._cleanup();
}
_cleanup() {
// clear obsolete cached resource
let clear = false;
Object.entries(this.resourceCache).forEach((entry) => {
const [id, updated] = entry;
if (!updated.time) {
// clear old version cache
delete this.resourceCache[id];
$.delete(`#${id}`);
clear = true;
}
if (new Date().getTime() - updated.time > this.expires) {
delete this.resourceCache[id];
clear = true;
}
});
if (clear) this._persist();
}
revokeAll() {
this.resourceCache = {};
this._persist();
}
_persist() {
$.write(JSON.stringify(this.resourceCache), SCRIPT_RESOURCE_CACHE_KEY);
}
get(id) {
const updated = this.resourceCache[id] && this.resourceCache[id].time;
if (updated && new Date().getTime() - updated <= this.expires) {
return this.resourceCache[id].data;
}
return null;
}
gettime(id) {
const updated = this.resourceCache[id] && this.resourceCache[id].time;
if (updated && new Date().getTime() - updated <= this.expires) {
return this.resourceCache[id].time;
}
return null;
}
set(id, value) {
this.resourceCache[id] = { time: new Date().getTime(), data: value };
this._persist();
}
}
function getExpiredTime() {
// console.log($.read(CSR_EXPIRATION_TIME_KEY));
if (!$.read(CSR_EXPIRATION_TIME_KEY)) {
$.write('1728e5', CSR_EXPIRATION_TIME_KEY); // 48 * 3600 * 1000
}
let expiration = 1728e5;
if ($.env.isLoon) {
const loont = {
// Loon 插件自义定
'1\u5206\u949f': 6e4,
'5\u5206\u949f': 3e5,
'10\u5206\u949f': 6e5,
'30\u5206\u949f': 18e5, // "30分钟"
'1\u5c0f\u65f6': 36e5,
'2\u5c0f\u65f6': 72e5,
'3\u5c0f\u65f6': 108e5,
'6\u5c0f\u65f6': 216e5,
'12\u5c0f\u65f6': 432e5,
'24\u5c0f\u65f6': 864e5,
'48\u5c0f\u65f6': 1728e5,
'72\u5c0f\u65f6': 2592e5, // "72小时"
'\u53c2\u6570\u4f20\u5165': 'readcachets', // "参数输入"
};
let intimed = $.read('#\u8282\u70b9\u7f13\u5b58\u6709\u6548\u671f'); // Loon #节点缓存有效期
// console.log(intimed);
if (intimed in loont) {
expiration = loont[intimed];
if (expiration === 'readcachets') {
expiration = intimed;
}
}
return expiration;
} else {
expiration = $.read(CSR_EXPIRATION_TIME_KEY);
return expiration;
}
}
export default new ResourceCache();

View File

@@ -17,7 +17,7 @@ export default function express({ substore: $, port }) {
const express_ = eval(`require("express")`);
const bodyParser = eval(`require("body-parser")`);
const app = express_();
app.use(bodyParser.json({ verify: rawBodySaver }));
app.use(bodyParser.json({ verify: rawBodySaver, limit: '1mb' }));
app.use(
bodyParser.urlencoded({ verify: rawBodySaver, extended: true }),
);

File diff suppressed because one or more lines are too long

View File

@@ -2,14 +2,18 @@
#!desc=高级订阅管理工具
#!openUrl=https://sub.store
#!author=Peng-YM
#!homepage=https://github.com/Peng-YM/Sub-Store
#!homepage=https://github.com/sub-store-org/Sub-Store
#!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png
#!select = 节点缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入
[Rule]
DOMAIN,sub-store.vercel.app,PROXY
[MITM]
hostname=sub.store
[Script]
http-request ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js, requires-body=true, timeout=120, tag=Sub-Store Core
http-request https?:\/\/sub\.store script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple
http-request ^https?:\/\/sub\.store script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple
cron "0 0 * * *" script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync

View File

@@ -1,20 +1,32 @@
# Sub-Store 配置指南
## 查看更新说明:
Sub-Store Releases: [`https://github.com/sub-store-org/Sub-Store/releases`](https://github.com/sub-store-org/Sub-Store/releases)
Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
## 脚本配置:
### 1. Loon
安装使用[插件](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Loon.plugin)即可。
安装使用 插件 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin) 即可。
### 2. Surge
安装使用[模块](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Surge.sgmodule)即可。
1. 官方默认版模块(目前不带 ability 参数, 不保证以后不会改动): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule)
2. 固定带 ability 参数版本,可能会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 请使用此带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule)
3. 固定不带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule)
### 3. QX
订阅[重写](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/QX.snippet)即可
订阅 重写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet) 即可
### 4. Stash
安装使用[ Stash 覆写](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Stash.stoverride)即可。
安装使用 覆写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride) 即可。
### 5. Shadowrocket
安装使用[模块](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Surge.sgmodule)即可。
安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule) 即可。
## 使用 Sub-Store
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。

View File

@@ -0,0 +1,12 @@
#!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数
[MITM]
hostname = %APPEND% sub.store
[Script]
# 主程序 已经去掉 Sub-Store Core 的参数 [,ability=http-client-policy] 不会爆内存,这个参数在 Surge 非常占用内存; 如果不需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 则可以使用此脚本
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true
Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js

View File

@@ -0,0 +1,11 @@
#!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 可能会爆内存, 如果不需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用不带 ability 参数版本
[MITM]
hostname = %APPEND% sub.store
[Script]
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120,ability=http-client-policy
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true
Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js

View File

@@ -1,10 +1,11 @@
#!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数
[MITM]
hostname=%APPEND% sub.store
hostname = %APPEND% sub.store
[Script]
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120,ability=http-client-policy
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true
Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js

View File

@@ -17,8 +17,13 @@ class ResourceCache {
let clear = false;
Object.entries(this.resourceCache).forEach((entry) => {
const [id, updated] = entry;
if (new Date().getTime() - updated > this.expires) {
if (!updated.time) {
// clear old version cache
delete this.resourceCache[id];
$.delete(`#${id}`);
clear = true;
}
if (new Date().getTime() - updated.time > this.expires) {
delete this.resourceCache[id];
clear = true;
}
@@ -27,9 +32,6 @@ class ResourceCache {
}
revokeAll() {
Object.keys(this.resourceCache).forEach((id) => {
$.delete(`#${id}`);
});
this.resourceCache = {};
this._persist();
}
@@ -39,17 +41,16 @@ class ResourceCache {
}
get(id) {
const updated = this.resourceCache[id];
const updated = this.resourceCache[id] && this.resourceCache[id].time;
if (updated && new Date().getTime() - updated <= this.expires) {
return $.read(`#${id}`);
return this.resourceCache[id].data;
}
return null;
}
set(id, value) {
this.resourceCache[id] = new Date().getTime();
this.resourceCache[id] = { time: new Date().getTime(), data: value }
this._persist();
$.write(value, `#${id}`);
}
}