mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
437297b8b0 | ||
|
|
ca437865e6 | ||
|
|
739100c873 | ||
|
|
a4384f4f13 | ||
|
|
468d136f0e | ||
|
|
b0c1157fe1 | ||
|
|
56626dabc7 | ||
|
|
2a87f7b3c3 | ||
|
|
81adfbc461 | ||
|
|
e04217c50d | ||
|
|
391a5aa2e4 | ||
|
|
2f3b42f552 | ||
|
|
76302f9d53 | ||
|
|
1924e9735c | ||
|
|
6a8cee3cd5 | ||
|
|
9b2209cc8b | ||
|
|
c585785c50 | ||
|
|
c3e5da7ee4 | ||
|
|
2ecad8bbb8 | ||
|
|
a642213928 | ||
|
|
f85e360ea8 | ||
|
|
1b948cdf52 | ||
|
|
556d5f393c | ||
|
|
a8c05207c0 | ||
|
|
e97fb1e6d9 | ||
|
|
e40b9a77c4 | ||
|
|
df0ac8a218 | ||
|
|
36377f3c20 | ||
|
|
47f26bdac8 | ||
|
|
a8a89ee2a2 | ||
|
|
199c5fb337 | ||
|
|
df505616ec | ||
|
|
c5b11f8b36 | ||
|
|
b19b49d2fa | ||
|
|
395c6e4e4a | ||
|
|
ae1c738f70 | ||
|
|
02d54208b0 | ||
|
|
5d3fc499ce | ||
|
|
d23bc7663e | ||
|
|
15704ea1c9 | ||
|
|
68cb393a7e | ||
|
|
2e99f28aa5 | ||
|
|
1a18e65e47 | ||
|
|
bbd5341d7a | ||
|
|
623802d73f | ||
|
|
19920dbfa3 | ||
|
|
8764e01d7e | ||
|
|
31b6dd0507 | ||
|
|
a84007d39e | ||
|
|
751e50bf99 | ||
|
|
a91f978042 | ||
|
|
1248e6b32a | ||
|
|
d0f255d9c6 | ||
|
|
060415584e | ||
|
|
8a2087c53a | ||
|
|
64117c50c7 | ||
|
|
6564d9497a | ||
|
|
1c03e46bbb | ||
|
|
084b385fdb | ||
|
|
290b9b5411 | ||
|
|
e5c1ae9ed8 | ||
|
|
9b6d9d49f9 | ||
|
|
a12adf5255 | ||
|
|
8682f14ee7 | ||
|
|
b3de7a4bc5 | ||
|
|
099ae5ad83 | ||
|
|
c7d00ac512 | ||
|
|
ca0d800bbb | ||
|
|
31b48d7a6c | ||
|
|
ab96ae9413 | ||
|
|
3fc507b576 | ||
|
|
2f2dbbdb68 | ||
|
|
1543e76841 | ||
|
|
74c4719806 | ||
|
|
b80d7f5875 | ||
|
|
779950ab11 | ||
|
|
42404537e8 | ||
|
|
228566116d | ||
|
|
9bb06bf438 | ||
|
|
88e52f9787 | ||
|
|
845a173738 | ||
|
|
4a6bcbc9b4 | ||
|
|
bbaac2de6f | ||
|
|
614438ae3d | ||
|
|
4966132397 | ||
|
|
059c4bd148 | ||
|
|
63887e3dad | ||
|
|
7fd585b5d4 | ||
|
|
16c79ac0fc | ||
|
|
14d9885db8 | ||
|
|
1e61088ed8 | ||
|
|
af6904ea50 | ||
|
|
1bc44ccde8 | ||
|
|
bdc7ee50f7 | ||
|
|
812f24d102 |
11
.github/workflows/main.yml
vendored
11
.github/workflows/main.yml
vendored
@@ -59,6 +59,17 @@ jobs:
|
||||
./backend/dist/sub-store-parser.loon.min.js
|
||||
./backend/dist/cron-sync-artifacts.min.js
|
||||
./backend/dist/sub-store.bundle.js
|
||||
- name: Git push assets to "release" branch
|
||||
run: |
|
||||
cd backend/dist || exit 1
|
||||
git init
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git checkout -b release
|
||||
git add .
|
||||
git commit -m "release: ${{ steps.tag.outputs.release_tag }}"
|
||||
git remote add origin "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}"
|
||||
git push -f -u origin release
|
||||
- name: Sync to GitLab
|
||||
env:
|
||||
GITLAB_PIPELINE_TOKEN: ${{ secrets.GITLAB_PIPELINE_TOKEN }}
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "web"]
|
||||
path = web
|
||||
url = https://github.com/sub-store-org/Sub-Store-Front-End.git
|
||||
|
||||
47
README.md
47
README.md
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
<br>
|
||||
<img width="200" src="https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png" alt="Sub-Store">
|
||||
<img width="200" src="https://raw.githubusercontent.com/cc63/ICON/main/Sub-Store.png" alt="Sub-Store">
|
||||
<br>
|
||||
<br>
|
||||
<h2 align="center">Sub-Store<h2>
|
||||
@@ -26,33 +26,32 @@ Core functionalities:
|
||||
|
||||
### Supported Input Formats
|
||||
|
||||
- [x] SS URI
|
||||
- [x] SSR URI
|
||||
- [x] SSD URI
|
||||
- [x] V2RayN URI
|
||||
- [x] Hysteria2 URI
|
||||
- [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] URI(SS, SSR, VMess, VLESS, Trojan, Hysteria, Hysteria 2, TUIC v5)
|
||||
- [x] Clash Proxies YAML
|
||||
- [x] Clash Proxy JSON(single line)
|
||||
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS)
|
||||
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, WireGuard, VLESS, Hysteria 2)
|
||||
- [x] Surge (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, TUIC, Snell, Hysteria 2, SSH(Password authentication only), SSR(external, only for macOS), External Proxy Program(only for macOS), WireGuard(Surge to Surge))
|
||||
- [x] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard(Surfboard to Surfboard))
|
||||
- [x] Shadowrocket (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC)
|
||||
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, 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] Plain JSON
|
||||
- [x] Stash
|
||||
- [x] Clash.Meta
|
||||
- [x] Clash.Meta(mihomo)
|
||||
- [x] Clash
|
||||
- [x] Surfboard
|
||||
- [x] Surge
|
||||
- [x] Loon
|
||||
- [x] Shadowrocket
|
||||
- [x] QX
|
||||
- [x] sing-box
|
||||
- [x] V2Ray
|
||||
- [x] V2Ray URI
|
||||
- [x] Plain JSON
|
||||
|
||||
## 2. Subscription Formatting
|
||||
|
||||
@@ -83,15 +82,25 @@ Install `pnpm`
|
||||
Go to `backend` directories, install node dependencies:
|
||||
|
||||
```
|
||||
pnpm install
|
||||
pnpm i
|
||||
```
|
||||
|
||||
1. In `backend`, run the backend server on http://localhost:3000
|
||||
|
||||
babel(old school)
|
||||
|
||||
```
|
||||
pnpm start
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
esbuild(experimental)
|
||||
|
||||
```
|
||||
pnpm run --parallel "/^dev:.*/"
|
||||
```
|
||||
|
||||
## LICENSE
|
||||
|
||||
This project is under the GPL V3 LICENSE.
|
||||
|
||||
77
backend/bundle-esbuild.js
Normal file
77
backend/bundle-esbuild.js
Normal file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { build } = require('esbuild');
|
||||
|
||||
!(async () => {
|
||||
const version = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'),
|
||||
).version.trim();
|
||||
|
||||
const artifacts = [
|
||||
{ src: 'src/main.js', dest: 'sub-store.min.js' },
|
||||
{
|
||||
src: 'src/products/resource-parser.loon.js',
|
||||
dest: 'dist/sub-store-parser.loon.min.js',
|
||||
},
|
||||
{
|
||||
src: 'src/products/cron-sync-artifacts.js',
|
||||
dest: 'dist/cron-sync-artifacts.min.js',
|
||||
},
|
||||
{ src: 'src/products/sub-store-0.js', dest: 'dist/sub-store-0.min.js' },
|
||||
{ src: 'src/products/sub-store-1.js', dest: 'dist/sub-store-1.min.js' },
|
||||
];
|
||||
|
||||
for await (const artifact of artifacts) {
|
||||
await build({
|
||||
entryPoints: [artifact.src],
|
||||
bundle: true,
|
||||
minify: true,
|
||||
sourcemap: false,
|
||||
platform: 'browser',
|
||||
format: 'iife',
|
||||
outfile: artifact.dest,
|
||||
});
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
);
|
||||
|
||||
await build({
|
||||
entryPoints: ['dist/sub-store.no-bundle.js'],
|
||||
bundle: true,
|
||||
minify: true,
|
||||
sourcemap: false,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
outfile: 'dist/sub-store.bundle.js',
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, 'dist/sub-store.bundle.js'),
|
||||
`// SUB_STORE_BACKEND_VERSION: ${version}
|
||||
${fs.readFileSync(path.join(__dirname, 'dist/sub-store.bundle.js'), {
|
||||
encoding: 'utf8',
|
||||
})}`,
|
||||
{
|
||||
encoding: 'utf8',
|
||||
},
|
||||
);
|
||||
})()
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
})
|
||||
.finally(() => {
|
||||
console.log('done');
|
||||
});
|
||||
@@ -3,23 +3,49 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { build } = require('esbuild');
|
||||
|
||||
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',
|
||||
});
|
||||
!(async () => {
|
||||
const version = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'),
|
||||
).version.trim();
|
||||
|
||||
build({
|
||||
entryPoints: ['dist/sub-store.no-bundle.js'],
|
||||
bundle: true,
|
||||
minify: true,
|
||||
sourcemap: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
outfile: 'dist/sub-store.bundle.js',
|
||||
});
|
||||
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',
|
||||
},
|
||||
);
|
||||
|
||||
await build({
|
||||
entryPoints: ['dist/sub-store.no-bundle.js'],
|
||||
bundle: true,
|
||||
minify: true,
|
||||
sourcemap: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
outfile: 'dist/sub-store.bundle.js',
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, 'dist/sub-store.bundle.js'),
|
||||
`// SUB_STORE_BACKEND_VERSION: ${version}
|
||||
${fs.readFileSync(path.join(__dirname, 'dist/sub-store.bundle.js'), {
|
||||
encoding: 'utf8',
|
||||
})}`,
|
||||
{
|
||||
encoding: 'utf8',
|
||||
},
|
||||
);
|
||||
})()
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
})
|
||||
.finally(() => {
|
||||
console.log('done');
|
||||
});
|
||||
|
||||
24
backend/dev-esbuild.js
Normal file
24
backend/dev-esbuild.js
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env node
|
||||
const { build } = require('esbuild');
|
||||
|
||||
!(async () => {
|
||||
const artifacts = [{ src: 'src/main.js', dest: 'sub-store.min.js' }];
|
||||
|
||||
for await (const artifact of artifacts) {
|
||||
await build({
|
||||
entryPoints: [artifact.src],
|
||||
bundle: true,
|
||||
minify: false,
|
||||
sourcemap: false,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
outfile: artifact.dest,
|
||||
});
|
||||
}
|
||||
})()
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
})
|
||||
.finally(() => {
|
||||
console.log('done');
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sub-store",
|
||||
"version": "2.14.166",
|
||||
"version": "2.14.239",
|
||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
@@ -8,6 +8,8 @@
|
||||
"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",
|
||||
"dev:esbuild": "nodemon -w src -w package.json dev-esbuild.js",
|
||||
"dev:run": "nodemon -w sub-store.min.js sub-store.min.js",
|
||||
"build": "gulp",
|
||||
"bundle": "node bundle.js"
|
||||
},
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import YAML from 'static-js-yaml';
|
||||
import YAML from '@/utils/yaml';
|
||||
import download from '@/utils/download';
|
||||
import { isIPv4, isIPv6, isValidPortNumber } from '@/utils';
|
||||
import {
|
||||
isIPv4,
|
||||
isIPv6,
|
||||
isValidPortNumber,
|
||||
isNotBlank,
|
||||
utf8ArrayToStr,
|
||||
} from '@/utils';
|
||||
import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
|
||||
import PROXY_PREPROCESSORS from './preprocessors';
|
||||
import PROXY_PRODUCERS from './producers';
|
||||
import PROXY_PARSERS from './parsers';
|
||||
import $ from '@/core/app';
|
||||
import { FILES_KEY, MODULES_KEY } from '@/constants';
|
||||
import { findByName } from '@/utils/database';
|
||||
import { produceArtifact } from '@/restful/sync';
|
||||
|
||||
function preprocess(raw) {
|
||||
for (const processor of PROXY_PREPROCESSORS) {
|
||||
@@ -63,7 +72,7 @@ function parse(raw) {
|
||||
return proxies;
|
||||
}
|
||||
|
||||
async function process(proxies, operators = [], targetPlatform, source) {
|
||||
async function processFn(proxies, operators = [], targetPlatform, source) {
|
||||
for (const item of operators) {
|
||||
// process script
|
||||
let script;
|
||||
@@ -95,18 +104,50 @@ async function process(proxies, operators = [], targetPlatform, source) {
|
||||
}
|
||||
}
|
||||
}
|
||||
url = `${url.split('#')[0]}${noCache ? '#noCache' : ''}`;
|
||||
const downloadUrlMatch = url.match(
|
||||
/^\/api\/(file|module)\/(.+)/,
|
||||
);
|
||||
if (downloadUrlMatch) {
|
||||
let type = '';
|
||||
try {
|
||||
type = downloadUrlMatch?.[1];
|
||||
let name = downloadUrlMatch?.[2];
|
||||
if (name == null) {
|
||||
throw new Error(`本地 ${type} URL 无效: ${url}`);
|
||||
}
|
||||
name = decodeURIComponent(name);
|
||||
const key = type === 'module' ? MODULES_KEY : FILES_KEY;
|
||||
const item = findByName($.read(key), name);
|
||||
if (!item) {
|
||||
throw new Error(`找不到 ${type}: ${name}`);
|
||||
}
|
||||
|
||||
// if this is a remote script, download it
|
||||
try {
|
||||
script = await download(
|
||||
`${url.split('#')[0]}${noCache ? '#noCache' : ''}`,
|
||||
);
|
||||
// $.info(`Script loaded: >>>\n ${script}`);
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`,
|
||||
);
|
||||
throw new Error(`无法下载脚本: ${url}`);
|
||||
if (type === 'module') {
|
||||
script = item.content;
|
||||
} else {
|
||||
script = await produceArtifact({
|
||||
type: 'file',
|
||||
name,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Error when loading ${type}: ${item.args.content}.\n Reason: ${err}`,
|
||||
);
|
||||
throw new Error(`无法加载 ${type}: ${url}`);
|
||||
}
|
||||
} else {
|
||||
// if this is a remote script, download it
|
||||
try {
|
||||
script = await download(url);
|
||||
// $.info(`Script loaded: >>>\n ${script}`);
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`,
|
||||
);
|
||||
throw new Error(`无法下载脚本: ${url}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
script = content;
|
||||
@@ -151,6 +192,13 @@ function produce(proxies, targetPlatform, type, opts = {}) {
|
||||
!(proxy.supported && proxy.supported[targetPlatform] === false),
|
||||
);
|
||||
|
||||
proxies = proxies.map((proxy) => {
|
||||
if (!isNotBlank(proxy.name)) {
|
||||
proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;
|
||||
}
|
||||
return proxy;
|
||||
});
|
||||
|
||||
$.info(`Producing proxies for target: ${targetPlatform}`);
|
||||
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
|
||||
let localPort = 10000;
|
||||
@@ -188,7 +236,7 @@ function produce(proxies, targetPlatform, type, opts = {}) {
|
||||
|
||||
export const ProxyUtils = {
|
||||
parse,
|
||||
process,
|
||||
process: processFn,
|
||||
produce,
|
||||
isIPv4,
|
||||
isIPv6,
|
||||
@@ -215,6 +263,10 @@ function safeMatch(parser, line) {
|
||||
}
|
||||
|
||||
function lastParse(proxy) {
|
||||
if (proxy.interface) {
|
||||
proxy['interface-name'] = proxy.interface;
|
||||
delete proxy.interface;
|
||||
}
|
||||
if (isValidPortNumber(proxy.port)) {
|
||||
proxy.port = parseInt(proxy.port, 10);
|
||||
}
|
||||
@@ -224,22 +276,56 @@ function lastParse(proxy) {
|
||||
.replace(/^\[/, '')
|
||||
.replace(/\]$/, '');
|
||||
}
|
||||
if (proxy.network === 'ws') {
|
||||
if (!proxy['ws-opts'] && (proxy['ws-path'] || proxy['ws-headers'])) {
|
||||
proxy['ws-opts'] = {};
|
||||
if (proxy['ws-path']) {
|
||||
proxy['ws-opts'].path = proxy['ws-path'];
|
||||
}
|
||||
if (proxy['ws-headers']) {
|
||||
proxy['ws-opts'].headers = proxy['ws-headers'];
|
||||
}
|
||||
}
|
||||
delete proxy['ws-path'];
|
||||
delete proxy['ws-headers'];
|
||||
}
|
||||
|
||||
if (proxy.type === 'trojan') {
|
||||
if (proxy.network === 'tcp') {
|
||||
delete proxy.network;
|
||||
}
|
||||
}
|
||||
if (['vless'].includes(proxy.type)) {
|
||||
if (!proxy.network) {
|
||||
proxy.network = 'tcp';
|
||||
}
|
||||
}
|
||||
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
|
||||
proxy.tls = true;
|
||||
}
|
||||
if (proxy.network) {
|
||||
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
let transporthost = proxy[`${proxy.network}-opts`]?.headers?.host;
|
||||
if (transporthost && !transportHost) {
|
||||
if (proxy.network === 'h2') {
|
||||
if (!transporthost && transportHost) {
|
||||
proxy[`${proxy.network}-opts`].headers.host = transportHost;
|
||||
delete proxy[`${proxy.network}-opts`].headers.Host;
|
||||
}
|
||||
} else if (transporthost && !transportHost) {
|
||||
proxy[`${proxy.network}-opts`].headers.Host = transporthost;
|
||||
delete proxy[`${proxy.network}-opts`].headers.host;
|
||||
}
|
||||
}
|
||||
if (proxy.network === 'h2') {
|
||||
const host = proxy['h2-opts']?.headers?.host;
|
||||
const path = proxy['h2-opts']?.path;
|
||||
if (host && !Array.isArray(host)) {
|
||||
proxy['h2-opts'].headers.host = [host];
|
||||
}
|
||||
if (Array.isArray(path)) {
|
||||
proxy['h2-opts'].path = path[0];
|
||||
}
|
||||
}
|
||||
if (proxy.tls && !proxy.sni) {
|
||||
if (proxy.network) {
|
||||
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
@@ -283,6 +369,29 @@ function lastParse(proxy) {
|
||||
if (['hysteria', 'hysteria2'].includes(proxy.type) && !proxy.ports) {
|
||||
delete proxy.ports;
|
||||
}
|
||||
if (['vless'].includes(proxy.type)) {
|
||||
if (['http'].includes(proxy.network)) {
|
||||
let transportPath = proxy[`${proxy.network}-opts`]?.path;
|
||||
if (!transportPath) {
|
||||
if (!proxy[`${proxy.network}-opts`]) {
|
||||
proxy[`${proxy.network}-opts`] = {};
|
||||
}
|
||||
proxy[`${proxy.network}-opts`].path = ['/'];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof proxy.name !== 'string') {
|
||||
try {
|
||||
if (proxy.name?.data) {
|
||||
proxy.name = Buffer.from(proxy.name.data).toString('utf8');
|
||||
} else {
|
||||
proxy.name = utf8ArrayToStr(proxy.name);
|
||||
}
|
||||
} catch (e) {
|
||||
$.error(`proxy.name decode failed\nReason: ${e}`);
|
||||
proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;
|
||||
}
|
||||
}
|
||||
return proxy;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { getIfNotBlank, isPresent, isNotBlank, getIfPresent } from '@/utils';
|
||||
import {
|
||||
isIPv4,
|
||||
isIPv6,
|
||||
getIfNotBlank,
|
||||
isPresent,
|
||||
isNotBlank,
|
||||
getIfPresent,
|
||||
} from '@/utils';
|
||||
import getSurgeParser from './peggy/surge';
|
||||
import getLoonParser from './peggy/loon';
|
||||
import getQXParser from './peggy/qx';
|
||||
@@ -25,8 +32,27 @@ function URI_SS() {
|
||||
// handle IPV4 and IPV6
|
||||
let serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
|
||||
let userInfoStr = Base64.decode(content.split('@')[0]);
|
||||
let query = '';
|
||||
if (!serverAndPortArray) {
|
||||
if (content.includes('?')) {
|
||||
const parsed = content.match(/^(.*)(\?.*)$/);
|
||||
content = parsed[1];
|
||||
query = parsed[2];
|
||||
}
|
||||
content = Base64.decode(content);
|
||||
if (query) {
|
||||
if (/(&|\?)v2ray-plugin=/.test(query)) {
|
||||
const parsed = query.match(/(&|\?)v2ray-plugin=(.*?)(&|$)/);
|
||||
let v2rayPlugin = parsed[2];
|
||||
if (v2rayPlugin) {
|
||||
proxy.plugin = 'v2ray-plugin';
|
||||
proxy['plugin-opts'] = JSON.parse(
|
||||
Base64.decode(v2rayPlugin),
|
||||
);
|
||||
}
|
||||
}
|
||||
content = `${content}${query}`;
|
||||
}
|
||||
userInfoStr = content.split('@')[0];
|
||||
serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
|
||||
}
|
||||
@@ -63,7 +89,7 @@ function URI_SS() {
|
||||
};
|
||||
break;
|
||||
case 'v2ray-plugin':
|
||||
proxy.obfs = 'v2ray-plugin';
|
||||
proxy.plugin = 'v2ray-plugin';
|
||||
proxy['plugin-opts'] = {
|
||||
mode: 'websocket',
|
||||
host: getIfNotBlank(params['obfs-host']),
|
||||
@@ -77,6 +103,12 @@ function URI_SS() {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (/(&|\?)uot=(1|true)/i.test(query)) {
|
||||
proxy['udp-over-tcp'] = true;
|
||||
}
|
||||
if (/(&|\?)tfo=(1|true)/i.test(query)) {
|
||||
proxy.tfo = true;
|
||||
}
|
||||
return proxy;
|
||||
};
|
||||
return { name, test, parse };
|
||||
@@ -241,11 +273,17 @@ function URI_VMess() {
|
||||
params.port = port;
|
||||
params.add = server;
|
||||
}
|
||||
const server = params.add;
|
||||
const port = parseInt(getIfPresent(params.port), 10);
|
||||
const proxy = {
|
||||
name: params.ps ?? params.remarks,
|
||||
name:
|
||||
params.ps ??
|
||||
params.remarks ??
|
||||
params.remark ??
|
||||
`VMess ${server}:${port}`,
|
||||
type: 'vmess',
|
||||
server: params.add,
|
||||
port: parseInt(getIfPresent(params.port), 10),
|
||||
server,
|
||||
port,
|
||||
cipher: getIfPresent(params.scy, 'auto'),
|
||||
uuid: params.id,
|
||||
alterId: parseInt(
|
||||
@@ -367,14 +405,18 @@ function URI_VLESS() {
|
||||
params[key] = value;
|
||||
}
|
||||
|
||||
proxy.name = name ?? params.remarks ?? `VLESS ${server}:${port}`;
|
||||
proxy.name =
|
||||
name ??
|
||||
params.remarks ??
|
||||
params.remark ??
|
||||
`VLESS ${server}:${port}`;
|
||||
|
||||
proxy.tls = params.security && params.security !== 'none';
|
||||
if (isShadowrocket && /TRUE|1/i.test(params.tls)) {
|
||||
proxy.tls = true;
|
||||
params.security = params.security ?? 'reality';
|
||||
}
|
||||
proxy.sni = params.sni ?? params.peer;
|
||||
proxy.sni = params.sni || params.peer;
|
||||
proxy.flow = params.flow;
|
||||
if (!proxy.flow && isShadowrocket && params.xtls) {
|
||||
// "none" is undefined
|
||||
@@ -402,21 +444,38 @@ function URI_VLESS() {
|
||||
proxy[`${params.security}-opts`] = opts;
|
||||
}
|
||||
}
|
||||
|
||||
proxy.network = params.type;
|
||||
if (proxy.network === 'tcp' && params.headerType === 'http') {
|
||||
proxy.network = 'http';
|
||||
}
|
||||
if (!proxy.network && isShadowrocket && params.obfs) {
|
||||
proxy.network = params.obfs;
|
||||
}
|
||||
if (['websocket'].includes(proxy.network)) {
|
||||
proxy.network = 'ws';
|
||||
}
|
||||
if (proxy.network && !['tcp', 'none'].includes(proxy.network)) {
|
||||
const opts = {};
|
||||
if (params.host) {
|
||||
opts.headers = { Host: params.host };
|
||||
const host = params.host ?? params.obfsParam;
|
||||
if (host) {
|
||||
if (params.obfsParam) {
|
||||
try {
|
||||
const parsed = JSON.parse(host);
|
||||
opts.headers = parsed;
|
||||
} catch (e) {
|
||||
opts.headers = { Host: host };
|
||||
}
|
||||
} else {
|
||||
opts.headers = { Host: host };
|
||||
}
|
||||
}
|
||||
if (params.serviceName) {
|
||||
opts[`${proxy.network}-service-name`] = params.serviceName;
|
||||
} else if (isShadowrocket && params.path) {
|
||||
opts[`${proxy.network}-service-name`] = params.path;
|
||||
delete params.path;
|
||||
if (!['ws', 'http', 'h2'].includes(proxy.network)) {
|
||||
opts[`${proxy.network}-service-name`] = params.path;
|
||||
delete params.path;
|
||||
}
|
||||
}
|
||||
if (params.path) {
|
||||
opts.path = params.path;
|
||||
@@ -496,6 +555,123 @@ function URI_Hysteria2() {
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
function URI_Hysteria() {
|
||||
const name = 'URI Hysteria Parser';
|
||||
const test = (line) => {
|
||||
return /^(hysteria|hy):\/\//.test(line);
|
||||
};
|
||||
const parse = (line) => {
|
||||
line = line.split(/(hysteria|hy):\/\//)[2];
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let [__, server, ___, port, ____, addons = '', name] =
|
||||
/^(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
|
||||
port = parseInt(`${port}`, 10);
|
||||
if (isNaN(port)) {
|
||||
port = 443;
|
||||
}
|
||||
if (name != null) {
|
||||
name = decodeURIComponent(name);
|
||||
}
|
||||
name = name ?? `Hysteria ${server}:${port}`;
|
||||
|
||||
const proxy = {
|
||||
type: 'hysteria',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
};
|
||||
const params = {};
|
||||
for (const addon of addons.split('&')) {
|
||||
let [key, value] = addon.split('=');
|
||||
key = key.replace(/_/, '-');
|
||||
value = decodeURIComponent(value);
|
||||
if (['alpn'].includes(key)) {
|
||||
proxy[key] = value ? value.split(',') : undefined;
|
||||
} else if (['insecure'].includes(key)) {
|
||||
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value);
|
||||
} else if (['auth'].includes(key)) {
|
||||
proxy['auth-str'] = value;
|
||||
} else if (['mport'].includes(key)) {
|
||||
proxy['ports'] = value;
|
||||
} else if (['obfsParam'].includes(key)) {
|
||||
proxy['obfs'] = value;
|
||||
} else if (['upmbps'].includes(key)) {
|
||||
proxy['up'] = value;
|
||||
} else if (['downmbps'].includes(key)) {
|
||||
proxy['down'] = value;
|
||||
} else if (['obfs'].includes(key)) {
|
||||
// obfs: Obfuscation mode (optional, empty or "xplus")
|
||||
proxy['_obfs'] = value || '';
|
||||
} else if (['fast-open', 'peer'].includes(key)) {
|
||||
params[key] = value;
|
||||
} else {
|
||||
proxy[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!proxy.sni && params.peer) {
|
||||
proxy.sni = params.peer;
|
||||
}
|
||||
if (!proxy['fast-open'] && params.fastopen) {
|
||||
proxy['fast-open'] = true;
|
||||
}
|
||||
if (!proxy.protocol) {
|
||||
// protocol: protocol to use ("udp", "wechat-video", "faketcp") (optional, default: "udp")
|
||||
proxy.protocol = 'udp';
|
||||
}
|
||||
|
||||
return proxy;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
function URI_TUIC() {
|
||||
const name = 'URI TUIC Parser';
|
||||
const test = (line) => {
|
||||
return /^tuic:\/\//.test(line);
|
||||
};
|
||||
const parse = (line) => {
|
||||
line = line.split(/tuic:\/\//)[1];
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let [__, uuid, password, server, ___, port, ____, addons = '', name] =
|
||||
/^(.*?):(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
|
||||
port = parseInt(`${port}`, 10);
|
||||
if (isNaN(port)) {
|
||||
port = 443;
|
||||
}
|
||||
password = decodeURIComponent(password);
|
||||
if (name != null) {
|
||||
name = decodeURIComponent(name);
|
||||
}
|
||||
name = name ?? `TUIC ${server}:${port}`;
|
||||
|
||||
const proxy = {
|
||||
type: 'tuic',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
password,
|
||||
uuid,
|
||||
};
|
||||
|
||||
for (const addon of addons.split('&')) {
|
||||
let [key, value] = addon.split('=');
|
||||
key = key.replace(/_/, '-');
|
||||
value = decodeURIComponent(value);
|
||||
if (['alpn'].includes(key)) {
|
||||
proxy[key] = value ? value.split(',') : undefined;
|
||||
} else if (['allow-insecure'].includes(key)) {
|
||||
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value);
|
||||
} else if (['disable-sni', 'reduce-rtt'].includes(key)) {
|
||||
proxy[key] = /(TRUE)|1/i.test(value);
|
||||
} else {
|
||||
proxy[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return proxy;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
// Trojan URI format
|
||||
function URI_Trojan() {
|
||||
@@ -569,9 +745,16 @@ function Clash_All() {
|
||||
}
|
||||
}
|
||||
|
||||
if (proxy.fingerprint) {
|
||||
proxy['tls-fingerprint'] = proxy.fingerprint;
|
||||
}
|
||||
|
||||
if (proxy['benchmark-url']) {
|
||||
proxy['test-url'] = proxy['benchmark-url'];
|
||||
}
|
||||
if (proxy['benchmark-timeout']) {
|
||||
proxy['test-timeout'] = proxy['benchmark-timeout'];
|
||||
}
|
||||
|
||||
return proxy;
|
||||
};
|
||||
@@ -614,6 +797,15 @@ function QX_VMess() {
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function QX_VLESS() {
|
||||
const name = 'QX VLESS Parser';
|
||||
const test = (line) => {
|
||||
return /^vless\s*=/.test(line.split(',')[0].trim());
|
||||
};
|
||||
const parse = (line) => getQXParser().parse(line);
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function QX_Trojan() {
|
||||
const name = 'QX Trojan Parser';
|
||||
const test = (line) => {
|
||||
@@ -824,6 +1016,14 @@ function Loon_WireGuard() {
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function Surge_SSH() {
|
||||
const name = 'Surge SSH Parser';
|
||||
const test = (line) => {
|
||||
return /^.*=\s*ssh/.test(line.split(',')[0]);
|
||||
};
|
||||
const parse = (line) => getSurgeParser().parse(line);
|
||||
return { name, test, parse };
|
||||
}
|
||||
function Surge_SS() {
|
||||
const name = 'Surge SS Parser';
|
||||
const test = (line) => {
|
||||
@@ -872,6 +1072,79 @@ function Surge_Socks5() {
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function Surge_External() {
|
||||
const name = 'Surge External Parser';
|
||||
const test = (line) => {
|
||||
return /^.*=\s*external/.test(line.split(',')[0]);
|
||||
};
|
||||
const parse = (line) => {
|
||||
let parsed = /^\s*(.*?)\s*?=\s*?external\s*?,\s*(.*?)\s*$/.exec(line);
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let [_, name, other] = parsed;
|
||||
line = other;
|
||||
|
||||
// exec = "/usr/bin/ssh" 或 exec = /usr/bin/ssh
|
||||
let exec = /(,|^)\s*?exec\s*?=\s*"(.*?)"\s*?(,|$)/.exec(line)?.[2];
|
||||
if (!exec) {
|
||||
exec = /(,|^)\s*?exec\s*?=\s*(.*?)\s*?(,|$)/.exec(line)?.[2];
|
||||
}
|
||||
|
||||
// local-port = "1080" 或 local-port = 1080
|
||||
let localPort = /(,|^)\s*?local-port\s*?=\s*"(.*?)"\s*?(,|$)/.exec(
|
||||
line,
|
||||
)?.[2];
|
||||
if (!localPort) {
|
||||
localPort = /(,|^)\s*?local-port\s*?=\s*(.*?)\s*?(,|$)/.exec(
|
||||
line,
|
||||
)?.[2];
|
||||
}
|
||||
// args = "-m", args = "rc4-md5"
|
||||
// args = -m, args = rc4-md5
|
||||
const argsRegex = /(,|^)\s*?args\s*?=\s*("(.*?)"|(.*?))(?=\s*?(,|$))/g;
|
||||
let argsMatch;
|
||||
const args = [];
|
||||
while ((argsMatch = argsRegex.exec(line)) !== null) {
|
||||
if (argsMatch[3] != null) {
|
||||
args.push(argsMatch[3]);
|
||||
} else if (argsMatch[4] != null) {
|
||||
args.push(argsMatch[4]);
|
||||
}
|
||||
}
|
||||
// addresses = "[ipv6]",,addresses = "ipv6", addresses = "ipv4"
|
||||
// addresses = [ipv6], addresses = ipv6, addresses = ipv4
|
||||
const addressesRegex =
|
||||
/(,|^)\s*?addresses\s*?=\s*("(.*?)"|(.*?))(?=\s*?(,|$))/g;
|
||||
let addressesMatch;
|
||||
const addresses = [];
|
||||
while ((addressesMatch = addressesRegex.exec(line)) !== null) {
|
||||
let ip;
|
||||
if (addressesMatch[3] != null) {
|
||||
ip = addressesMatch[3];
|
||||
} else if (addressesMatch[4] != null) {
|
||||
ip = addressesMatch[4];
|
||||
}
|
||||
if (ip != null) {
|
||||
ip = `${ip}`.trim().replace(/^\[/, '').replace(/\]$/, '');
|
||||
}
|
||||
if (isIP(ip)) {
|
||||
addresses.push(ip);
|
||||
}
|
||||
}
|
||||
|
||||
const proxy = {
|
||||
type: 'external',
|
||||
name,
|
||||
exec,
|
||||
'local-port': localPort,
|
||||
args,
|
||||
addresses,
|
||||
};
|
||||
return proxy;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function Surge_Snell() {
|
||||
const name = 'Surge Snell Parser';
|
||||
const test = (line) => {
|
||||
@@ -907,14 +1180,21 @@ function Surge_Hysteria2() {
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function isIP(ip) {
|
||||
return isIPv4(ip) || isIPv6(ip);
|
||||
}
|
||||
|
||||
export default [
|
||||
URI_SS(),
|
||||
URI_SSR(),
|
||||
URI_VMess(),
|
||||
URI_VLESS(),
|
||||
URI_TUIC(),
|
||||
URI_Hysteria(),
|
||||
URI_Hysteria2(),
|
||||
URI_Trojan(),
|
||||
Clash_All(),
|
||||
Surge_SSH(),
|
||||
Surge_SS(),
|
||||
Surge_VMess(),
|
||||
Surge_Trojan(),
|
||||
@@ -924,6 +1204,7 @@ export default [
|
||||
Surge_WireGuard(),
|
||||
Surge_Hysteria2(),
|
||||
Surge_Socks5(),
|
||||
Surge_External(),
|
||||
Loon_SS(),
|
||||
Loon_SSR(),
|
||||
Loon_VMess(),
|
||||
@@ -935,6 +1216,7 @@ export default [
|
||||
QX_SS(),
|
||||
QX_SSR(),
|
||||
QX_VMess(),
|
||||
QX_VLESS(),
|
||||
QX_Trojan(),
|
||||
QX_Http(),
|
||||
QX_Socks5(),
|
||||
|
||||
@@ -68,7 +68,7 @@ trojan = tag equals "trojan"i address password (transport/transport_host/transpo
|
||||
proxy.type = "trojan";
|
||||
handleTransport();
|
||||
}
|
||||
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/download_bandwidth/ecn/others)* {
|
||||
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/fast_open/download_bandwidth/ecn/others)* {
|
||||
proxy.type = "hysteria2";
|
||||
}
|
||||
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
@@ -117,7 +117,7 @@ port = digits:[0-9]+ {
|
||||
method = comma cipher:cipher {
|
||||
proxy.cipher = cipher;
|
||||
}
|
||||
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"auto");
|
||||
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"auto"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"rc4-md5"/"rc4"/"salsa20"/"xchacha20-ietf-poly1305");
|
||||
|
||||
username = & {
|
||||
let j = peg$currPos;
|
||||
|
||||
@@ -66,7 +66,7 @@ trojan = tag equals "trojan"i address password (transport/transport_host/transpo
|
||||
proxy.type = "trojan";
|
||||
handleTransport();
|
||||
}
|
||||
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/download_bandwidth/ecn/others)* {
|
||||
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/fast_open/download_bandwidth/ecn/others)* {
|
||||
proxy.type = "hysteria2";
|
||||
}
|
||||
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
@@ -115,7 +115,7 @@ port = digits:[0-9]+ {
|
||||
method = comma cipher:cipher {
|
||||
proxy.cipher = cipher;
|
||||
}
|
||||
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"auto");
|
||||
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"auto"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"rc4-md5"/"rc4"/"salsa20"/"xchacha20-ietf-poly1305");
|
||||
|
||||
username = & {
|
||||
let j = peg$currPos;
|
||||
|
||||
@@ -38,7 +38,7 @@ const grammars = String.raw`
|
||||
}
|
||||
}
|
||||
|
||||
start = (trojan/shadowsocks/vmess/http/socks5) {
|
||||
start = (trojan/shadowsocks/vmess/vless/http/socks5) {
|
||||
return proxy
|
||||
}
|
||||
|
||||
@@ -91,6 +91,13 @@ vmess = "vmess" equals address
|
||||
handleObfs();
|
||||
}
|
||||
|
||||
vless = "vless" equals address
|
||||
(uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/others)* {
|
||||
proxy.type = "vless";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
handleObfs();
|
||||
}
|
||||
|
||||
http = "http" equals address
|
||||
(username/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)*{
|
||||
proxy.type = "http";
|
||||
@@ -142,7 +149,7 @@ uuid = comma "password" equals uuid:[^=,]+ { proxy.uuid = uuid.join("").trim();
|
||||
method = comma "method" equals cipher:cipher {
|
||||
proxy.cipher = cipher;
|
||||
};
|
||||
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
|
||||
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"none"/"rc2-cfb"/"rc4-md5-6"/"rc4-md5"/"salsa20"/"xchacha20-ietf-poly1305");
|
||||
aead = comma "aead" equals flag:bool { proxy.aead = flag; }
|
||||
|
||||
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
start = (trojan/shadowsocks/vmess/http/socks5) {
|
||||
start = (trojan/shadowsocks/vmess/vless/http/socks5) {
|
||||
return proxy
|
||||
}
|
||||
|
||||
@@ -89,6 +89,13 @@ vmess = "vmess" equals address
|
||||
handleObfs();
|
||||
}
|
||||
|
||||
vless = "vless" equals address
|
||||
(uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/others)* {
|
||||
proxy.type = "vless";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
handleObfs();
|
||||
}
|
||||
|
||||
http = "http" equals address
|
||||
(username/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)*{
|
||||
proxy.type = "http";
|
||||
@@ -140,7 +147,7 @@ uuid = comma "password" equals uuid:[^=,]+ { proxy.uuid = uuid.join("").trim();
|
||||
method = comma "method" equals cipher:cipher {
|
||||
proxy.cipher = cipher;
|
||||
};
|
||||
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
|
||||
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"none"/"rc2-cfb"/"rc4-md5-6"/"rc4-md5"/"salsa20"/"xchacha20-ietf-poly1305");
|
||||
aead = comma "aead" equals flag:bool { proxy.aead = flag; }
|
||||
|
||||
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
|
||||
|
||||
@@ -30,13 +30,18 @@ const grammars = String.raw`
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleShadowTLS() {
|
||||
if (proxy['shadow-tls-password'] && !proxy['shadow-tls-version']) {
|
||||
proxy['shadow-tls-version'] = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
|
||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh) {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "ss";
|
||||
// handle obfs
|
||||
if (obfs.type == "http" || obfs.type === "tls") {
|
||||
@@ -45,8 +50,9 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
|
||||
$set(proxy, "plugin-opts.host", obfs.host);
|
||||
$set(proxy, "plugin-opts.path", obfs.path);
|
||||
}
|
||||
handleShadowTLS();
|
||||
}
|
||||
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "vmess";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
if (proxy.aead) {
|
||||
@@ -55,19 +61,27 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
|
||||
proxy.alterId = proxy.alterId || 0;
|
||||
}
|
||||
handleWebsocket();
|
||||
handleShadowTLS();
|
||||
}
|
||||
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "trojan";
|
||||
handleWebsocket();
|
||||
handleShadowTLS();
|
||||
}
|
||||
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "http";
|
||||
proxy.tls = true;
|
||||
handleShadowTLS();
|
||||
}
|
||||
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "http";
|
||||
handleShadowTLS();
|
||||
}
|
||||
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "ssh";
|
||||
handleShadowTLS();
|
||||
}
|
||||
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "snell";
|
||||
// handle obfs
|
||||
if (obfs.type == "http" || obfs.type === "tls") {
|
||||
@@ -75,26 +89,33 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
|
||||
$set(proxy, "obfs-opts.host", obfs.host);
|
||||
$set(proxy, "obfs-opts.path", obfs.path);
|
||||
}
|
||||
handleShadowTLS();
|
||||
}
|
||||
tuic = tag equals "tuic" address (alpn/token/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "tuic";
|
||||
handleShadowTLS();
|
||||
}
|
||||
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "tuic";
|
||||
proxy.version = 5;
|
||||
handleShadowTLS();
|
||||
}
|
||||
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "wireguard-surge";
|
||||
handleShadowTLS();
|
||||
}
|
||||
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "hysteria2";
|
||||
handleShadowTLS();
|
||||
}
|
||||
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "socks5";
|
||||
handleShadowTLS();
|
||||
}
|
||||
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "socks5";
|
||||
proxy.tls = true;
|
||||
handleShadowTLS();
|
||||
}
|
||||
|
||||
address = comma server:server comma port:port {
|
||||
@@ -171,7 +192,7 @@ vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
|
||||
method = comma "encrypt-method" equals cipher:cipher {
|
||||
proxy.cipher = cipher;
|
||||
}
|
||||
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
|
||||
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"idea-cfb"/"none"/"rc2-cfb"/"rc4-md5"/"rc4"/"salsa20"/"seed-cfb"/"xchacha20-ietf-poly1305");
|
||||
|
||||
ws = comma "ws" equals flag:bool { obfs.type = "ws"; }
|
||||
ws_headers = comma "ws-headers" equals headers:$[^,]+ {
|
||||
@@ -201,6 +222,14 @@ no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-ale
|
||||
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
|
||||
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
|
||||
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
|
||||
test_udp = comma "test-udp" equals match:[^,]+ { proxy["test-udp"] = match.join(""); }
|
||||
test_timeout = comma "test-timeout" equals match:$[0-9]+ { proxy["test-timeout"] = parseInt(match.trim()); }
|
||||
tos = comma "tos" equals match:$[0-9]+ { proxy.tos = parseInt(match.trim()); }
|
||||
interface = comma "interface" equals match:[^,]+ { proxy.interface = match.join(""); }
|
||||
allow_other_interface = comma "allow-other-interface" equals flag:bool { proxy["allow-other-interface"] = flag; }
|
||||
hybrid = comma "hybrid" equals flag:bool { proxy.hybrid = flag; }
|
||||
idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"] = parseInt(match.trim()); }
|
||||
server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
|
||||
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
|
||||
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
|
||||
|
||||
@@ -28,13 +28,18 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleShadowTLS() {
|
||||
if (proxy['shadow-tls-password'] && !proxy['shadow-tls-version']) {
|
||||
proxy['shadow-tls-version'] = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
|
||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh) {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "ss";
|
||||
// handle obfs
|
||||
if (obfs.type == "http" || obfs.type === "tls") {
|
||||
@@ -43,8 +48,9 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
|
||||
$set(proxy, "plugin-opts.host", obfs.host);
|
||||
$set(proxy, "plugin-opts.path", obfs.path);
|
||||
}
|
||||
handleShadowTLS();
|
||||
}
|
||||
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "vmess";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
if (proxy.aead) {
|
||||
@@ -53,19 +59,27 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
|
||||
proxy.alterId = proxy.alterId || 0;
|
||||
}
|
||||
handleWebsocket();
|
||||
handleShadowTLS();
|
||||
}
|
||||
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "trojan";
|
||||
handleWebsocket();
|
||||
handleShadowTLS();
|
||||
}
|
||||
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "http";
|
||||
proxy.tls = true;
|
||||
handleShadowTLS();
|
||||
}
|
||||
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "http";
|
||||
handleShadowTLS();
|
||||
}
|
||||
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "ssh";
|
||||
handleShadowTLS();
|
||||
}
|
||||
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "snell";
|
||||
// handle obfs
|
||||
if (obfs.type == "http" || obfs.type === "tls") {
|
||||
@@ -73,26 +87,33 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
|
||||
$set(proxy, "obfs-opts.host", obfs.host);
|
||||
$set(proxy, "obfs-opts.path", obfs.path);
|
||||
}
|
||||
handleShadowTLS();
|
||||
}
|
||||
tuic = tag equals "tuic" address (alpn/token/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "tuic";
|
||||
handleShadowTLS();
|
||||
}
|
||||
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "tuic";
|
||||
proxy.version = 5;
|
||||
handleShadowTLS();
|
||||
}
|
||||
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "wireguard-surge";
|
||||
handleShadowTLS();
|
||||
}
|
||||
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "hysteria2";
|
||||
handleShadowTLS();
|
||||
}
|
||||
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "socks5";
|
||||
handleShadowTLS();
|
||||
}
|
||||
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "socks5";
|
||||
proxy.tls = true;
|
||||
handleShadowTLS();
|
||||
}
|
||||
|
||||
address = comma server:server comma port:port {
|
||||
@@ -169,7 +190,7 @@ vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
|
||||
method = comma "encrypt-method" equals cipher:cipher {
|
||||
proxy.cipher = cipher;
|
||||
}
|
||||
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
|
||||
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"idea-cfb"/"none"/"rc2-cfb"/"rc4-md5"/"rc4"/"salsa20"/"seed-cfb"/"xchacha20-ietf-poly1305");
|
||||
|
||||
ws = comma "ws" equals flag:bool { obfs.type = "ws"; }
|
||||
ws_headers = comma "ws-headers" equals headers:$[^,]+ {
|
||||
@@ -199,6 +220,14 @@ no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-ale
|
||||
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
|
||||
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
|
||||
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
|
||||
test_udp = comma "test-udp" equals match:[^,]+ { proxy["test-udp"] = match.join(""); }
|
||||
test_timeout = comma "test-timeout" equals match:$[0-9]+ { proxy["test-timeout"] = parseInt(match.trim()); }
|
||||
tos = comma "tos" equals match:$[0-9]+ { proxy.tos = parseInt(match.trim()); }
|
||||
interface = comma "interface" equals match:[^,]+ { proxy.interface = match.join(""); }
|
||||
allow_other_interface = comma "allow-other-interface" equals flag:bool { proxy["allow-other-interface"] = flag; }
|
||||
hybrid = comma "hybrid" equals flag:bool { proxy.hybrid = flag; }
|
||||
idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"] = parseInt(match.trim()); }
|
||||
server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
|
||||
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
|
||||
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
|
||||
|
||||
@@ -30,7 +30,7 @@ start = (trojan) {
|
||||
return proxy
|
||||
}
|
||||
|
||||
trojan = "trojan://" password:password "@" server:server ":" port:port params? name:name?{
|
||||
trojan = "trojan://" password:password "@" server:server ":" port:port "/"? params? name:name?{
|
||||
proxy.type = "trojan";
|
||||
proxy.password = password;
|
||||
proxy.server = server;
|
||||
@@ -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"];
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ start = (trojan) {
|
||||
return proxy
|
||||
}
|
||||
|
||||
trojan = "trojan://" password:password "@" server:server ":" port:port params? name:name?{
|
||||
trojan = "trojan://" password:password "@" server:server ":" port:port "/"? params? name:name?{
|
||||
proxy.type = "trojan";
|
||||
proxy.password = password;
|
||||
proxy.server = server;
|
||||
@@ -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"];
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { safeLoad } from 'static-js-yaml';
|
||||
import { safeLoad } from '@/utils/yaml';
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
function HTML() {
|
||||
@@ -46,8 +46,19 @@ function Clash() {
|
||||
};
|
||||
const parse = function (raw) {
|
||||
// Clash YAML format
|
||||
const proxies = safeLoad(raw).proxies;
|
||||
return proxies.map((p) => JSON.stringify(p)).join('\n');
|
||||
const {
|
||||
proxies,
|
||||
'global-client-fingerprint': globalClientFingerprint,
|
||||
} = safeLoad(raw);
|
||||
return proxies
|
||||
.map((p) => {
|
||||
// https://github.com/MetaCubeX/mihomo/blob/Alpha/docs/config.yaml#L73C1-L73C26
|
||||
if (globalClientFingerprint && !p['client-fingerprint']) {
|
||||
p['client-fingerprint'] = globalClientFingerprint;
|
||||
}
|
||||
return JSON.stringify(p);
|
||||
})
|
||||
.join('\n');
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
getFlowField,
|
||||
getFlowHeaders,
|
||||
parseFlowHeaders,
|
||||
validCheck,
|
||||
flowTransfer,
|
||||
} from '@/utils/flow';
|
||||
|
||||
@@ -86,12 +87,15 @@ function QuickSettingOperator(args) {
|
||||
if (get(args.useless)) {
|
||||
const filter = UselessFilter();
|
||||
const selected = filter.func(proxies);
|
||||
proxies.filter((_, i) => selected[i]);
|
||||
proxies = proxies.filter(
|
||||
(p, i) => selected[i] && p.port > 0 && p.port <= 65535,
|
||||
);
|
||||
}
|
||||
|
||||
return proxies.map((proxy) => {
|
||||
proxy.udp = get(args.udp, proxy.udp);
|
||||
proxy.tfo = get(args.tfo, proxy.tfo);
|
||||
proxy['fast-open'] = get(args.tfo, proxy['fast-open']);
|
||||
proxy['skip-cert-verify'] = get(
|
||||
args.scert,
|
||||
proxy['skip-cert-verify'],
|
||||
@@ -117,7 +121,7 @@ function QuickSettingOperator(args) {
|
||||
}
|
||||
|
||||
// add or remove flag for proxies
|
||||
function FlagOperator({ mode }) {
|
||||
function FlagOperator({ mode, tw }) {
|
||||
return {
|
||||
name: 'Flag Operator',
|
||||
func: (proxies) => {
|
||||
@@ -131,7 +135,13 @@ function FlagOperator({ mode }) {
|
||||
// remove old flag
|
||||
proxy.name = removeFlag(proxy.name);
|
||||
proxy.name = newFlag + ' ' + proxy.name;
|
||||
proxy.name = proxy.name.replace(/🇹🇼/g, '🇨🇳');
|
||||
if (tw == 'ws') {
|
||||
proxy.name = proxy.name.replace(/🇹🇼/g, '🇼🇸');
|
||||
} else if (tw == 'tw') {
|
||||
// 不变
|
||||
} else {
|
||||
proxy.name = proxy.name.replace(/🇹🇼/g, '🇨🇳');
|
||||
}
|
||||
}
|
||||
return proxy;
|
||||
});
|
||||
@@ -324,7 +334,7 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
|
||||
const operator = createDynamicFunction(
|
||||
'operator',
|
||||
`async function operator(input = []) {
|
||||
if (input?.$files || input?.$content) {
|
||||
if (input && (input.$files || input.$content)) {
|
||||
let { $content, $files } = input
|
||||
${script}
|
||||
return { $content, $files }
|
||||
@@ -347,15 +357,45 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
|
||||
};
|
||||
}
|
||||
|
||||
function parseIP4P(IP4P) {
|
||||
let server;
|
||||
let port;
|
||||
try {
|
||||
if (!/^2001::[^:]+:[^:]+:[^:]+$/.test(IP4P)) {
|
||||
throw new Error(`Invalid IP4P: ${IP4P}`);
|
||||
}
|
||||
let array = IP4P.split(':');
|
||||
|
||||
port = parseInt(array[2], 16);
|
||||
let ipab = parseInt(array[3], 16);
|
||||
let ipcd = parseInt(array[4], 16);
|
||||
let ipa = ipab >> 8;
|
||||
let ipb = ipab & 0xff;
|
||||
let ipc = ipcd >> 8;
|
||||
let ipd = ipcd & 0xff;
|
||||
server = `${ipa}.${ipb}.${ipc}.${ipd}`;
|
||||
if (port <= 0 || port > 65535) {
|
||||
throw new Error(`Invalid port number: ${port}`);
|
||||
}
|
||||
if (!isIPv4(server)) {
|
||||
throw new Error(`Invalid IP address: ${server}`);
|
||||
}
|
||||
} catch (e) {
|
||||
// throw new Error(`IP4P 解析失败: ${e}`);
|
||||
$.error(`IP4P 解析失败: ${e}`);
|
||||
}
|
||||
return { server, port };
|
||||
}
|
||||
|
||||
const DOMAIN_RESOLVERS = {
|
||||
Google: async function (domain) {
|
||||
const id = hex_md5(`GOOGLE:${domain}`);
|
||||
Google: async function (domain, type, noCache) {
|
||||
const id = hex_md5(`GOOGLE:${domain}:${type}`);
|
||||
const cached = resourceCache.get(id);
|
||||
if (cached) return cached;
|
||||
if (!noCache && cached) return cached;
|
||||
const resp = await $.http.get({
|
||||
url: `https://8.8.4.4/resolve?name=${encodeURIComponent(
|
||||
domain,
|
||||
)}&type=A`,
|
||||
)}&type=${type === 'IPv6' ? 'AAAA' : 'A'}`,
|
||||
headers: {
|
||||
accept: 'application/dns-json',
|
||||
},
|
||||
@@ -372,10 +412,13 @@ const DOMAIN_RESOLVERS = {
|
||||
resourceCache.set(id, result);
|
||||
return result;
|
||||
},
|
||||
'IP-API': async function (domain) {
|
||||
'IP-API': async function (domain, type, noCache) {
|
||||
if (['IPv6'].includes(type)) {
|
||||
throw new Error(`域名解析服务提供方 IP-API 不支持 ${type}`);
|
||||
}
|
||||
const id = hex_md5(`IP-API:${domain}`);
|
||||
const cached = resourceCache.get(id);
|
||||
if (cached) return cached;
|
||||
if (!noCache && cached) return cached;
|
||||
const resp = await $.http.get({
|
||||
url: `http://ip-api.com/json/${encodeURIComponent(
|
||||
domain,
|
||||
@@ -389,14 +432,14 @@ const DOMAIN_RESOLVERS = {
|
||||
resourceCache.set(id, result);
|
||||
return result;
|
||||
},
|
||||
Cloudflare: async function (domain) {
|
||||
const id = hex_md5(`CLOUDFLARE:${domain}`);
|
||||
Cloudflare: async function (domain, type, noCache) {
|
||||
const id = hex_md5(`CLOUDFLARE:${domain}:${type}`);
|
||||
const cached = resourceCache.get(id);
|
||||
if (cached) return cached;
|
||||
if (!noCache && cached) return cached;
|
||||
const resp = await $.http.get({
|
||||
url: `https://1.0.0.1/dns-query?name=${encodeURIComponent(
|
||||
domain,
|
||||
)}&type=A`,
|
||||
)}&type=${type === 'IPv6' ? 'AAAA' : 'A'}`,
|
||||
headers: {
|
||||
accept: 'application/dns-json',
|
||||
},
|
||||
@@ -413,14 +456,14 @@ const DOMAIN_RESOLVERS = {
|
||||
resourceCache.set(id, result);
|
||||
return result;
|
||||
},
|
||||
Ali: async function (domain) {
|
||||
const id = hex_md5(`ALI:${domain}`);
|
||||
Ali: async function (domain, type, noCache) {
|
||||
const id = hex_md5(`ALI:${domain}:${type}`);
|
||||
const cached = resourceCache.get(id);
|
||||
if (cached) return cached;
|
||||
if (!noCache && cached) return cached;
|
||||
const resp = await $.http.get({
|
||||
url: `http://223.6.6.6/resolve?name=${encodeURIComponent(
|
||||
domain,
|
||||
)}&type=A&short=1`,
|
||||
)}&type=${type === 'IPv6' ? 'AAAA' : 'A'}&short=1`,
|
||||
headers: {
|
||||
accept: 'application/dns-json',
|
||||
},
|
||||
@@ -433,14 +476,14 @@ const DOMAIN_RESOLVERS = {
|
||||
resourceCache.set(id, result);
|
||||
return result;
|
||||
},
|
||||
Tencent: async function (domain) {
|
||||
const id = hex_md5(`ALI:${domain}`);
|
||||
Tencent: async function (domain, type, noCache) {
|
||||
const id = hex_md5(`ALI:${domain}:${type}`);
|
||||
const cached = resourceCache.get(id);
|
||||
if (cached) return cached;
|
||||
if (!noCache && cached) return cached;
|
||||
const resp = await $.http.get({
|
||||
url: `http://119.28.28.28/d?type=A&dn=${encodeURIComponent(
|
||||
domain,
|
||||
)}`,
|
||||
url: `http://119.28.28.28/d?type=${
|
||||
type === 'IPv6' ? 'AAAA' : 'A'
|
||||
}&dn=${encodeURIComponent(domain)}`,
|
||||
headers: {
|
||||
accept: 'application/dns-json',
|
||||
},
|
||||
@@ -455,10 +498,15 @@ const DOMAIN_RESOLVERS = {
|
||||
},
|
||||
};
|
||||
|
||||
function ResolveDomainOperator({ provider }) {
|
||||
function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
|
||||
if (['IPv6', 'IP4P'].includes(_type) && ['IP-API'].includes(provider)) {
|
||||
throw new Error(`域名解析服务提供方 ${provider} 不支持 ${_type}`);
|
||||
}
|
||||
let type = ['IPv6', 'IP4P'].includes(_type) ? 'IPv6' : 'IPv4';
|
||||
|
||||
const resolver = DOMAIN_RESOLVERS[provider];
|
||||
if (!resolver) {
|
||||
throw new Error(`Cannot find resolver: ${provider}`);
|
||||
throw new Error(`找不到域名解析服务提供方: ${provider}`);
|
||||
}
|
||||
return {
|
||||
name: 'Resolve Domain Operator',
|
||||
@@ -477,7 +525,7 @@ function ResolveDomainOperator({ provider }) {
|
||||
const currentBatch = [];
|
||||
for (let domain of totalDomain.splice(0, limit)) {
|
||||
currentBatch.push(
|
||||
resolver(domain)
|
||||
resolver(domain, type, cache === 'disabled')
|
||||
.then((ip) => {
|
||||
results[domain] = ip;
|
||||
$.info(
|
||||
@@ -496,15 +544,38 @@ function ResolveDomainOperator({ provider }) {
|
||||
proxies.forEach((p) => {
|
||||
if (!p['no-resolve']) {
|
||||
if (results[p.server]) {
|
||||
p.server = results[p.server];
|
||||
p.resolved = true;
|
||||
if (_type === 'IP4P') {
|
||||
const { server, port } = parseIP4P(
|
||||
results[p.server],
|
||||
);
|
||||
if (server && port) {
|
||||
p.server = server;
|
||||
p.port = port;
|
||||
p.resolved = true;
|
||||
}
|
||||
} else {
|
||||
p.server = results[p.server];
|
||||
p.resolved = true;
|
||||
}
|
||||
} else {
|
||||
p.resolved = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return proxies;
|
||||
return proxies.filter((p) => {
|
||||
if (filter === 'removeFailed') {
|
||||
return isIP(p.server) || p['no-resolve'] || p.resolved;
|
||||
} else if (filter === 'IPOnly') {
|
||||
return isIP(p.server);
|
||||
} else if (filter === 'IPv4Only') {
|
||||
return isIPv4(p.server);
|
||||
} else if (filter === 'IPv6Only') {
|
||||
return isIPv6(p.server);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -791,6 +862,7 @@ function createDynamicFunction(name, script, $arguments) {
|
||||
getFlowHeaders,
|
||||
parseFlowHeaders,
|
||||
flowTransfer,
|
||||
validCheck,
|
||||
};
|
||||
if ($.env.isLoon) {
|
||||
return new Function(
|
||||
|
||||
@@ -107,6 +107,31 @@ export default function Clash_Producer() {
|
||||
proxy['http-opts'].headers.Host = [httpHost];
|
||||
}
|
||||
}
|
||||
if (
|
||||
['vmess', 'vless'].includes(proxy.type) &&
|
||||
proxy.network === 'h2'
|
||||
) {
|
||||
let path = proxy['h2-opts']?.path;
|
||||
if (
|
||||
isPresent(proxy, 'h2-opts.path') &&
|
||||
Array.isArray(path)
|
||||
) {
|
||||
proxy['h2-opts'].path = path[0];
|
||||
}
|
||||
let host = proxy['h2-opts']?.headers?.host;
|
||||
if (
|
||||
isPresent(proxy, 'h2-opts.headers.Host') &&
|
||||
!Array.isArray(host)
|
||||
) {
|
||||
proxy['h2-opts'].headers.host = [host];
|
||||
}
|
||||
}
|
||||
if (proxy['plugin-opts']?.tls) {
|
||||
if (isPresent(proxy, 'skip-cert-verify')) {
|
||||
proxy['plugin-opts']['skip-cert-verify'] =
|
||||
proxy['skip-cert-verify'];
|
||||
}
|
||||
}
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||
proxy.type,
|
||||
@@ -119,6 +144,10 @@ export default function Clash_Producer() {
|
||||
proxy.fingerprint = proxy['tls-fingerprint'];
|
||||
}
|
||||
delete proxy['tls-fingerprint'];
|
||||
if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
|
||||
delete proxy.tls;
|
||||
}
|
||||
|
||||
delete proxy.subName;
|
||||
delete proxy.collectionName;
|
||||
if (
|
||||
|
||||
@@ -89,6 +89,18 @@ export default function ClashMeta_Producer() {
|
||||
proxy.servername = proxy.sni;
|
||||
delete proxy.sni;
|
||||
}
|
||||
} else if (proxy.type === 'ss') {
|
||||
if (
|
||||
isPresent(proxy, 'shadow-tls-password') &&
|
||||
!isPresent(proxy, 'plugin')
|
||||
) {
|
||||
proxy.plugin = 'shadow-tls';
|
||||
proxy['plugin-opts'] = {
|
||||
host: proxy['shadow-tls-sni'],
|
||||
password: proxy['shadow-tls-password'],
|
||||
version: proxy['shadow-tls-version'],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -110,7 +122,32 @@ export default function ClashMeta_Producer() {
|
||||
proxy['http-opts'].headers.Host = [httpHost];
|
||||
}
|
||||
}
|
||||
if (
|
||||
['vmess', 'vless'].includes(proxy.type) &&
|
||||
proxy.network === 'h2'
|
||||
) {
|
||||
let path = proxy['h2-opts']?.path;
|
||||
if (
|
||||
isPresent(proxy, 'h2-opts.path') &&
|
||||
Array.isArray(path)
|
||||
) {
|
||||
proxy['h2-opts'].path = path[0];
|
||||
}
|
||||
let host = proxy['h2-opts']?.headers?.host;
|
||||
if (
|
||||
isPresent(proxy, 'h2-opts.headers.Host') &&
|
||||
!Array.isArray(host)
|
||||
) {
|
||||
proxy['h2-opts'].headers.host = [host];
|
||||
}
|
||||
}
|
||||
|
||||
if (proxy['plugin-opts']?.tls) {
|
||||
if (isPresent(proxy, 'skip-cert-verify')) {
|
||||
proxy['plugin-opts']['skip-cert-verify'] =
|
||||
proxy['skip-cert-verify'];
|
||||
}
|
||||
}
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||
proxy.type,
|
||||
@@ -123,6 +160,9 @@ export default function ClashMeta_Producer() {
|
||||
proxy.fingerprint = proxy['tls-fingerprint'];
|
||||
}
|
||||
delete proxy['tls-fingerprint'];
|
||||
if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
|
||||
delete proxy.tls;
|
||||
}
|
||||
delete proxy.subName;
|
||||
delete proxy.collectionName;
|
||||
if (
|
||||
|
||||
@@ -32,6 +32,32 @@ export default function Loon_Producer() {
|
||||
|
||||
function shadowsocks(proxy) {
|
||||
const result = new Result(proxy);
|
||||
if (
|
||||
![
|
||||
'rc4',
|
||||
'rc4-md5',
|
||||
'aes-128-cfb',
|
||||
'aes-192-cfb',
|
||||
'aes-256-cfb',
|
||||
'aes-128-ctr',
|
||||
'aes-192-ctr',
|
||||
'aes-256-ctr',
|
||||
'bf-cfb',
|
||||
'camellia-128-cfb',
|
||||
'camellia-192-cfb',
|
||||
'camellia-256-cfb',
|
||||
'salsa20',
|
||||
'chacha20',
|
||||
'chacha20-ietf',
|
||||
'aes-128-gcm',
|
||||
'aes-192-gcm',
|
||||
'aes-256-gcm',
|
||||
'chacha20-ietf-poly1305',
|
||||
'xchacha20-ietf-poly1305',
|
||||
].includes(proxy.cipher)
|
||||
) {
|
||||
throw new Error(`cipher ${proxy.cipher} is not supported`);
|
||||
}
|
||||
result.append(
|
||||
`${proxy.name}=shadowsocks,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}"`,
|
||||
);
|
||||
@@ -57,7 +83,9 @@ function shadowsocks(proxy) {
|
||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
|
||||
if (proxy.udp) {
|
||||
result.append(`,udp=true`);
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
@@ -83,7 +111,9 @@ function shadowsocksr(proxy) {
|
||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
|
||||
if (proxy.udp) {
|
||||
result.append(`,udp=true`);
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
@@ -93,7 +123,9 @@ function trojan(proxy) {
|
||||
result.append(
|
||||
`${proxy.name}=trojan,${proxy.server},${proxy.port},"${proxy.password}"`,
|
||||
);
|
||||
|
||||
if (proxy.network === 'tcp') {
|
||||
delete proxy.network;
|
||||
}
|
||||
// transport
|
||||
if (isPresent(proxy, 'network')) {
|
||||
if (proxy.network === 'ws') {
|
||||
@@ -124,7 +156,9 @@ function trojan(proxy) {
|
||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
|
||||
if (proxy.udp) {
|
||||
result.append(`,udp=true`);
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
@@ -134,7 +168,9 @@ function vmess(proxy) {
|
||||
result.append(
|
||||
`${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.uuid}"`,
|
||||
);
|
||||
|
||||
if (proxy.network === 'tcp') {
|
||||
delete proxy.network;
|
||||
}
|
||||
// transport
|
||||
if (isPresent(proxy, 'network')) {
|
||||
if (proxy.network === 'ws') {
|
||||
@@ -189,19 +225,23 @@ function vmess(proxy) {
|
||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
|
||||
if (proxy.udp) {
|
||||
result.append(`,udp=true`);
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function vless(proxy) {
|
||||
if (proxy['reality-opts']) {
|
||||
throw new Error(`reality is unsupported`);
|
||||
throw new Error(`VLESS REALITY is unsupported`);
|
||||
}
|
||||
const result = new Result(proxy);
|
||||
result.append(
|
||||
`${proxy.name}=vless,${proxy.server},${proxy.port},"${proxy.uuid}"`,
|
||||
);
|
||||
|
||||
if (proxy.network === 'tcp') {
|
||||
delete proxy.network;
|
||||
}
|
||||
// transport
|
||||
if (isPresent(proxy, 'network')) {
|
||||
if (proxy.network === 'ws') {
|
||||
@@ -249,7 +289,9 @@ function vless(proxy) {
|
||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
|
||||
if (proxy.udp) {
|
||||
result.append(`,udp=true`);
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -272,8 +314,6 @@ function http(proxy) {
|
||||
// tfo
|
||||
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -304,7 +344,11 @@ function wireguard(proxy) {
|
||||
if (proxy.dns) {
|
||||
if (Array.isArray(proxy.dns)) {
|
||||
proxy.dnsv6 = proxy.dns.find((i) => isIPv6(i));
|
||||
proxy.dns = proxy.dns.find((i) => isIPv4(i));
|
||||
let dns = proxy.dns.find((i) => isIPv4(i));
|
||||
if (!dns) {
|
||||
dns = proxy.dns.find((i) => !isIPv4(i) && !isIPv6(i));
|
||||
}
|
||||
proxy.dns = dns;
|
||||
}
|
||||
}
|
||||
result.appendIfPresent(`,dns=${proxy.dns}`, 'dns');
|
||||
@@ -354,8 +398,13 @@ function hysteria2(proxy) {
|
||||
'skip-cert-verify',
|
||||
);
|
||||
|
||||
// tfo
|
||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
|
||||
if (proxy.udp) {
|
||||
result.append(`,udp=true`);
|
||||
}
|
||||
|
||||
// download-bandwidth
|
||||
result.appendIfPresent(
|
||||
|
||||
@@ -3,7 +3,7 @@ import { isPresent, Result } from './utils';
|
||||
const targetPlatform = 'QX';
|
||||
|
||||
export default function QX_Producer() {
|
||||
const produce = (proxy) => {
|
||||
const produce = (proxy, type, opts = {}) => {
|
||||
switch (proxy.type) {
|
||||
case 'ss':
|
||||
return shadowsocks(proxy);
|
||||
@@ -17,6 +17,14 @@ export default function QX_Producer() {
|
||||
return http(proxy);
|
||||
case 'socks5':
|
||||
return socks5(proxy);
|
||||
case 'vless':
|
||||
if (opts['include-unsupported-proxy']) {
|
||||
return vless(proxy);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Platform ${targetPlatform}(App Store Release) does not support proxy type: ${proxy.type}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
||||
@@ -29,7 +37,36 @@ function shadowsocks(proxy) {
|
||||
const result = new Result(proxy);
|
||||
const append = result.append.bind(result);
|
||||
const appendIfPresent = result.appendIfPresent.bind(result);
|
||||
|
||||
if (!proxy.cipher) {
|
||||
proxy.cipher = 'none';
|
||||
}
|
||||
if (
|
||||
![
|
||||
'none',
|
||||
'rc4-md5',
|
||||
'rc4-md5-6',
|
||||
'aes-128-cfb',
|
||||
'aes-192-cfb',
|
||||
'aes-256-cfb',
|
||||
'aes-128-ctr',
|
||||
'aes-192-ctr',
|
||||
'aes-256-ctr',
|
||||
'bf-cfb',
|
||||
'cast5-cfb',
|
||||
'des-cfb',
|
||||
'rc2-cfb',
|
||||
'salsa20',
|
||||
'chacha20',
|
||||
'chacha20-ietf',
|
||||
'aes-128-gcm',
|
||||
'aes-192-gcm',
|
||||
'aes-256-gcm',
|
||||
'chacha20-ietf-poly1305',
|
||||
'xchacha20-ietf-poly1305',
|
||||
].includes(proxy.cipher)
|
||||
) {
|
||||
throw new Error(`cipher ${proxy.cipher} is not supported`);
|
||||
}
|
||||
append(`shadowsocks=${proxy.server}:${proxy.port}`);
|
||||
append(`,method=${proxy.cipher}`);
|
||||
append(`,password=${proxy.password}`);
|
||||
@@ -325,6 +362,105 @@ function vmess(proxy) {
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
function vless(proxy) {
|
||||
if (typeof proxy.flow !== 'undefined' || proxy['reality-opts']) {
|
||||
throw new Error(`VLESS XTLS/REALITY is not supported`);
|
||||
}
|
||||
|
||||
const result = new Result(proxy);
|
||||
const append = result.append.bind(result);
|
||||
const appendIfPresent = result.appendIfPresent.bind(result);
|
||||
|
||||
append(`vless=${proxy.server}:${proxy.port}`);
|
||||
|
||||
// The method field for vless should be none.
|
||||
let cipher = 'none';
|
||||
// if (proxy.cipher === 'auto') {
|
||||
// cipher = 'chacha20-ietf-poly1305';
|
||||
// } else {
|
||||
// cipher = proxy.cipher;
|
||||
// }
|
||||
append(`,method=${cipher}`);
|
||||
|
||||
append(`,password=${proxy.uuid}`);
|
||||
|
||||
// obfs
|
||||
if (needTls(proxy)) {
|
||||
proxy.tls = true;
|
||||
}
|
||||
if (isPresent(proxy, 'network')) {
|
||||
if (proxy.network === 'ws') {
|
||||
if (proxy.tls) append(`,obfs=wss`);
|
||||
else append(`,obfs=ws`);
|
||||
} else if (proxy.network === 'http') {
|
||||
append(`,obfs=http`);
|
||||
} else if (!['tcp'].includes(proxy.network)) {
|
||||
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=${
|
||||
Array.isArray(transportPath) ? transportPath[0] : transportPath
|
||||
}`,
|
||||
`${proxy.network}-opts.path`,
|
||||
);
|
||||
appendIfPresent(
|
||||
`,obfs-host=${
|
||||
Array.isArray(transportHost) ? transportHost[0] : transportHost
|
||||
}`,
|
||||
`${proxy.network}-opts.headers.Host`,
|
||||
);
|
||||
} else {
|
||||
// over-tls
|
||||
if (proxy.tls) append(`,obfs=over-tls`);
|
||||
}
|
||||
|
||||
if (needTls(proxy)) {
|
||||
appendIfPresent(
|
||||
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
|
||||
'tls-pubkey-sha256',
|
||||
);
|
||||
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
|
||||
appendIfPresent(
|
||||
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
|
||||
'tls-no-session-ticket',
|
||||
);
|
||||
appendIfPresent(
|
||||
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
|
||||
'tls-no-session-reuse',
|
||||
);
|
||||
// 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');
|
||||
}
|
||||
|
||||
// tfo
|
||||
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// udp
|
||||
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// server_check_url
|
||||
result.appendIfPresent(
|
||||
`,server_check_url=${proxy['test-url']}`,
|
||||
'test-url',
|
||||
);
|
||||
|
||||
// tag
|
||||
append(`,tag=${proxy.name}`);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function http(proxy) {
|
||||
const result = new Result(proxy);
|
||||
|
||||
@@ -126,7 +126,31 @@ export default function ShadowRocket_Producer() {
|
||||
proxy['http-opts'].headers.Host = [httpHost];
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
['vmess', 'vless'].includes(proxy.type) &&
|
||||
proxy.network === 'h2'
|
||||
) {
|
||||
let path = proxy['h2-opts']?.path;
|
||||
if (
|
||||
isPresent(proxy, 'h2-opts.path') &&
|
||||
Array.isArray(path)
|
||||
) {
|
||||
proxy['h2-opts'].path = path[0];
|
||||
}
|
||||
let host = proxy['h2-opts']?.headers?.host;
|
||||
if (
|
||||
isPresent(proxy, 'h2-opts.headers.Host') &&
|
||||
!Array.isArray(host)
|
||||
) {
|
||||
proxy['h2-opts'].headers.host = [host];
|
||||
}
|
||||
}
|
||||
if (proxy['plugin-opts']?.tls) {
|
||||
if (isPresent(proxy, 'skip-cert-verify')) {
|
||||
proxy['plugin-opts']['skip-cert-verify'] =
|
||||
proxy['skip-cert-verify'];
|
||||
}
|
||||
}
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||
proxy.type,
|
||||
@@ -139,6 +163,9 @@ export default function ShadowRocket_Producer() {
|
||||
proxy.fingerprint = proxy['tls-fingerprint'];
|
||||
}
|
||||
delete proxy['tls-fingerprint'];
|
||||
if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
|
||||
delete proxy.tls;
|
||||
}
|
||||
delete proxy.subName;
|
||||
delete proxy.collectionName;
|
||||
if (
|
||||
|
||||
@@ -178,8 +178,8 @@ const grpcParser = (proxy, parsedProxy) => {
|
||||
const transport = { type: 'grpc' };
|
||||
if (proxy['grpc-opts']) {
|
||||
const serviceName = proxy['grpc-opts']['grpc-service-name'];
|
||||
if (serviceName && serviceName !== '')
|
||||
transport.service_name = serviceName;
|
||||
if (serviceName != null && serviceName !== '')
|
||||
transport.service_name = `${serviceName}`;
|
||||
}
|
||||
parsedProxy.transport = transport;
|
||||
};
|
||||
@@ -217,6 +217,31 @@ const tlsParser = (proxy, parsedProxy) => {
|
||||
if (!parsedProxy.tls.enabled) delete parsedProxy.tls;
|
||||
};
|
||||
|
||||
const sshParser = (proxy = {}) => {
|
||||
const parsedProxy = {
|
||||
tag: proxy.name,
|
||||
type: 'ssh',
|
||||
server: proxy.server,
|
||||
server_port: parseInt(`${proxy.port}`, 10),
|
||||
};
|
||||
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy.username) parsedProxy.user = proxy.username;
|
||||
if (proxy.password) parsedProxy.password = proxy.password;
|
||||
if (proxy['server-fingerprint']) {
|
||||
parsedProxy.host_key = [proxy['server-fingerprint']];
|
||||
// https://manual.nssurge.com/policy/ssh.html
|
||||
// Surge only supports curve25519-sha256 as the kex algorithm and aes128-gcm as the encryption algorithm. It means that the SSH server must use OpenSSH v7.3 or above. (It should not be a problem since OpenSSH 7.3 was released on 2016-08-01.)
|
||||
// TODO: ?
|
||||
parsedProxy.host_key_algorithms = [
|
||||
proxy['server-fingerprint'].split(' ')[0],
|
||||
];
|
||||
}
|
||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||
tfoParser(proxy, parsedProxy);
|
||||
return parsedProxy;
|
||||
};
|
||||
|
||||
const httpParser = (proxy = {}) => {
|
||||
const parsedProxy = {
|
||||
tag: proxy.name,
|
||||
@@ -225,7 +250,7 @@ const httpParser = (proxy = {}) => {
|
||||
server_port: parseInt(`${proxy.port}`, 10),
|
||||
tls: { enabled: false, server_name: proxy.server, insecure: false },
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy.username) parsedProxy.username = proxy.username;
|
||||
if (proxy.password) parsedProxy.password = proxy.password;
|
||||
@@ -252,7 +277,7 @@ const socks5Parser = (proxy = {}) => {
|
||||
password: proxy.password,
|
||||
version: '5',
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy.username) parsedProxy.username = proxy.username;
|
||||
if (proxy.password) parsedProxy.password = proxy.password;
|
||||
@@ -263,6 +288,37 @@ const socks5Parser = (proxy = {}) => {
|
||||
return parsedProxy;
|
||||
};
|
||||
|
||||
const shadowTLSParser = (proxy = {}) => {
|
||||
const ssPart = {
|
||||
tag: proxy.name,
|
||||
type: 'shadowsocks',
|
||||
method: proxy.cipher,
|
||||
password: proxy.password,
|
||||
detour: `${proxy.name}_shadowtls`,
|
||||
};
|
||||
const stPart = {
|
||||
tag: `${proxy.name}_shadowtls`,
|
||||
type: 'shadowtls',
|
||||
server: proxy.server,
|
||||
server_port: parseInt(`${proxy.port}`, 10),
|
||||
version: proxy['plugin-opts'].version,
|
||||
password: proxy['plugin-opts'].password,
|
||||
tls: {
|
||||
enabled: true,
|
||||
server_name: proxy['plugin-opts'].host,
|
||||
utls: {
|
||||
enabled: true,
|
||||
fingerprint: proxy['client-fingerprint'],
|
||||
},
|
||||
},
|
||||
};
|
||||
if (stPart.server_port < 0 || stPart.server_port > 65535)
|
||||
throw '端口值非法';
|
||||
if (proxy['fast-open'] === true) stPart.udp_fragment = true;
|
||||
tfoParser(proxy, stPart);
|
||||
smuxParser(proxy.smux, ssPart);
|
||||
return { type: 'ss-with-st', ssPart, stPart };
|
||||
};
|
||||
const ssParser = (proxy = {}) => {
|
||||
const parsedProxy = {
|
||||
tag: proxy.name,
|
||||
@@ -272,7 +328,7 @@ const ssParser = (proxy = {}) => {
|
||||
method: proxy.cipher,
|
||||
password: proxy.password,
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy.uot) parsedProxy.udp_over_tcp = true;
|
||||
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
|
||||
@@ -348,7 +404,7 @@ const ssrParser = (proxy = {}) => {
|
||||
obfs: proxy.obfs,
|
||||
protocol: proxy.protocol,
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy['obfs-param']) parsedProxy.obfs_param = proxy['obfs-param'];
|
||||
if (proxy['protocol-param'] && proxy['protocol-param'] !== '')
|
||||
@@ -381,7 +437,7 @@ const vmessParser = (proxy = {}) => {
|
||||
].indexOf(parsedProxy.security) === -1
|
||||
)
|
||||
parsedProxy.security = 'auto';
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy.xudp) parsedProxy.packet_encoding = 'xudp';
|
||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||
@@ -405,7 +461,7 @@ const vlessParser = (proxy = {}) => {
|
||||
uuid: proxy.uuid,
|
||||
tls: { enabled: false, server_name: proxy.server, insecure: false },
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||
if (proxy.flow === 'xtls-rprx-vision') parsedProxy.flow = proxy.flow;
|
||||
@@ -426,7 +482,7 @@ const trojanParser = (proxy = {}) => {
|
||||
password: proxy.password,
|
||||
tls: { enabled: true, server_name: proxy.server, insecure: false },
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
|
||||
@@ -446,7 +502,7 @@ const hysteriaParser = (proxy = {}) => {
|
||||
disable_mtu_discovery: false,
|
||||
tls: { enabled: true, server_name: proxy.server, insecure: false },
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy.auth_str) parsedProxy.auth_str = `${proxy.auth_str}`;
|
||||
if (proxy['auth-str']) parsedProxy.auth_str = `${proxy['auth-str']}`;
|
||||
@@ -493,7 +549,7 @@ const hysteria2Parser = (proxy = {}) => {
|
||||
obfs: {},
|
||||
tls: { enabled: true, server_name: proxy.server, insecure: false },
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy.up) parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
|
||||
if (proxy.down) parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
|
||||
@@ -516,7 +572,7 @@ const tuic5Parser = (proxy = {}) => {
|
||||
password: proxy.password,
|
||||
tls: { enabled: true, server_name: proxy.server, insecure: false },
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||
if (
|
||||
@@ -537,24 +593,30 @@ const tuic5Parser = (proxy = {}) => {
|
||||
};
|
||||
|
||||
const wireguardParser = (proxy = {}) => {
|
||||
const local_address = ['ip', 'ipv6']
|
||||
.map((i) => proxy[i])
|
||||
.filter((i) => i)
|
||||
.map((i) => (/\\/.test(i) ? i : `${i}/32`));
|
||||
const parsedProxy = {
|
||||
tag: proxy.name,
|
||||
type: 'wireguard',
|
||||
server: proxy.server,
|
||||
server_port: parseInt(`${proxy.port}`, 10),
|
||||
local_address: [proxy.ip, proxy.ipv6],
|
||||
local_address,
|
||||
private_key: proxy['private-key'],
|
||||
peer_public_key: proxy['public-key'],
|
||||
pre_shared_key: proxy['pre-shared-key'],
|
||||
reserved: [],
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||
if (typeof proxy.reserved === 'string') {
|
||||
parsedProxy.reserved.push(proxy.reserved);
|
||||
} else {
|
||||
} else if (Array.isArray(proxy.reserved)) {
|
||||
for (const r of proxy.reserved) parsedProxy.reserved.push(r);
|
||||
} else {
|
||||
delete parsedProxy.reserved;
|
||||
}
|
||||
if (proxy.peers && proxy.peers.length > 0) {
|
||||
parsedProxy.peers = [];
|
||||
@@ -563,13 +625,15 @@ const wireguardParser = (proxy = {}) => {
|
||||
server: p.server,
|
||||
server_port: parseInt(`${p.port}`, 10),
|
||||
public_key: p['public-key'],
|
||||
allowed_ips: p.allowed_ips,
|
||||
allowed_ips: p['allowed-ips'] || p.allowed_ips,
|
||||
reserved: [],
|
||||
};
|
||||
if (typeof p.reserved === 'string') {
|
||||
peer.reserved.push(p.reserved);
|
||||
} else {
|
||||
} else if (Array.isArray(p.reserved)) {
|
||||
for (const r of p.reserved) peer.reserved.push(r);
|
||||
} else {
|
||||
delete peer.reserved;
|
||||
}
|
||||
if (p['pre-shared-key']) peer.pre_shared_key = p['pre-shared-key'];
|
||||
parsedProxy.peers.push(peer);
|
||||
@@ -589,6 +653,9 @@ export default function singbox_Producer() {
|
||||
.map((proxy) => {
|
||||
try {
|
||||
switch (proxy.type) {
|
||||
case 'ssh':
|
||||
list.push(sshParser(proxy));
|
||||
break;
|
||||
case 'http':
|
||||
list.push(httpParser(proxy));
|
||||
break;
|
||||
@@ -602,10 +669,40 @@ export default function singbox_Producer() {
|
||||
}
|
||||
break;
|
||||
case 'ss':
|
||||
// if (!proxy.cipher) {
|
||||
// proxy.cipher = 'none';
|
||||
// }
|
||||
// if (
|
||||
// ![
|
||||
// '2022-blake3-aes-128-gcm',
|
||||
// '2022-blake3-aes-256-gcm',
|
||||
// '2022-blake3-chacha20-poly1305',
|
||||
// 'aes-128-cfb',
|
||||
// 'aes-128-ctr',
|
||||
// 'aes-128-gcm',
|
||||
// 'aes-192-cfb',
|
||||
// 'aes-192-ctr',
|
||||
// 'aes-192-gcm',
|
||||
// 'aes-256-cfb',
|
||||
// 'aes-256-ctr',
|
||||
// 'aes-256-gcm',
|
||||
// 'chacha20-ietf',
|
||||
// 'chacha20-ietf-poly1305',
|
||||
// 'none',
|
||||
// 'rc4-md5',
|
||||
// 'xchacha20',
|
||||
// 'xchacha20-ietf-poly1305',
|
||||
// ].includes(proxy.cipher)
|
||||
// ) {
|
||||
// throw new Error(
|
||||
// `cipher ${proxy.cipher} is not supported`,
|
||||
// );
|
||||
// }
|
||||
if (proxy.plugin === 'shadow-tls') {
|
||||
throw new Error(
|
||||
`Platform sing-box does not support proxy type: ${proxy.type} with shadow-tls`,
|
||||
);
|
||||
const { ssPart, stPart } =
|
||||
shadowTLSParser(proxy);
|
||||
list.push(ssPart);
|
||||
list.push(stPart);
|
||||
} else {
|
||||
list.push(ssParser(proxy));
|
||||
}
|
||||
|
||||
@@ -2,241 +2,275 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
|
||||
|
||||
export default function Stash_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies) => {
|
||||
const produce = (proxies, type, opts = {}) => {
|
||||
// https://stash.wiki/proxy-protocols/proxy-types#shadowsocks
|
||||
return (
|
||||
'proxies:\n' +
|
||||
proxies
|
||||
.filter((proxy) => {
|
||||
if (
|
||||
const list = proxies
|
||||
.filter((proxy) => {
|
||||
if (opts['include-unsupported-proxy']) return true;
|
||||
if (
|
||||
![
|
||||
'ss',
|
||||
'ssr',
|
||||
'vmess',
|
||||
'socks5',
|
||||
'http',
|
||||
'snell',
|
||||
'trojan',
|
||||
'tuic',
|
||||
'vless',
|
||||
'wireguard',
|
||||
'hysteria',
|
||||
'hysteria2',
|
||||
].includes(proxy.type) ||
|
||||
(proxy.type === 'ss' &&
|
||||
![
|
||||
'ss',
|
||||
'ssr',
|
||||
'vmess',
|
||||
'socks5',
|
||||
'http',
|
||||
'snell',
|
||||
'trojan',
|
||||
'tuic',
|
||||
'vless',
|
||||
'wireguard',
|
||||
'hysteria',
|
||||
'hysteria2',
|
||||
].includes(proxy.type) ||
|
||||
(proxy.type === 'ss' &&
|
||||
![
|
||||
'aes-128-gcm',
|
||||
'aes-192-gcm',
|
||||
'aes-256-gcm',
|
||||
'aes-128-cfb',
|
||||
'aes-192-cfb',
|
||||
'aes-256-cfb',
|
||||
'aes-128-ctr',
|
||||
'aes-192-ctr',
|
||||
'aes-256-ctr',
|
||||
'rc4-md5',
|
||||
'chacha20-ietf',
|
||||
'xchacha20',
|
||||
'chacha20-ietf-poly1305',
|
||||
'xchacha20-ietf-poly1305',
|
||||
].includes(proxy.cipher)) ||
|
||||
(proxy.type === 'snell' &&
|
||||
String(proxy.version) === '4') ||
|
||||
(proxy.type === 'vless' && proxy['reality-opts'])
|
||||
) {
|
||||
return false;
|
||||
'aes-128-gcm',
|
||||
'aes-192-gcm',
|
||||
'aes-256-gcm',
|
||||
'aes-128-cfb',
|
||||
'aes-192-cfb',
|
||||
'aes-256-cfb',
|
||||
'aes-128-ctr',
|
||||
'aes-192-ctr',
|
||||
'aes-256-ctr',
|
||||
'rc4-md5',
|
||||
'chacha20-ietf',
|
||||
'xchacha20',
|
||||
'chacha20-ietf-poly1305',
|
||||
'xchacha20-ietf-poly1305',
|
||||
].includes(proxy.cipher)) ||
|
||||
(proxy.type === 'snell' && String(proxy.version) === '4') ||
|
||||
(proxy.type === 'vless' && proxy['reality-opts'])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((proxy) => {
|
||||
if (proxy.type === 'vmess') {
|
||||
// handle vmess aead
|
||||
if (isPresent(proxy, 'aead')) {
|
||||
if (proxy.aead) {
|
||||
proxy.alterId = 0;
|
||||
}
|
||||
delete proxy.aead;
|
||||
}
|
||||
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;
|
||||
delete proxy.tfo;
|
||||
}
|
||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
|
||||
if (
|
||||
(!proxy.token || proxy.token.length === 0) &&
|
||||
!isPresent(proxy, 'version')
|
||||
) {
|
||||
proxy.version = 5;
|
||||
}
|
||||
} else if (proxy.type === 'hysteria') {
|
||||
// auth_str 将会在未来某个时候删除 但是有的机场不规范
|
||||
if (
|
||||
isPresent(proxy, 'auth_str') &&
|
||||
!isPresent(proxy, 'auth-str')
|
||||
) {
|
||||
proxy['auth-str'] = proxy['auth_str'];
|
||||
}
|
||||
if (isPresent(proxy, 'alpn')) {
|
||||
proxy.alpn = Array.isArray(proxy.alpn)
|
||||
? proxy.alpn
|
||||
: [proxy.alpn];
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'tfo') &&
|
||||
!isPresent(proxy, 'fast-open')
|
||||
) {
|
||||
proxy['fast-open'] = proxy.tfo;
|
||||
delete proxy.tfo;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'down') &&
|
||||
!isPresent(proxy, 'down-speed')
|
||||
) {
|
||||
proxy['down-speed'] = proxy.down;
|
||||
delete proxy.down;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'up') &&
|
||||
!isPresent(proxy, 'up-speed')
|
||||
) {
|
||||
proxy['up-speed'] = proxy.up;
|
||||
delete proxy.up;
|
||||
}
|
||||
if (isPresent(proxy, 'down-speed')) {
|
||||
proxy['down-speed'] =
|
||||
`${proxy['down-speed']}`.match(/\d+/)?.[0] || 0;
|
||||
}
|
||||
if (isPresent(proxy, 'up-speed')) {
|
||||
proxy['up-speed'] =
|
||||
`${proxy['up-speed']}`.match(/\d+/)?.[0] || 0;
|
||||
}
|
||||
} else if (proxy.type === 'hysteria2') {
|
||||
if (
|
||||
isPresent(proxy, 'password') &&
|
||||
!isPresent(proxy, 'auth')
|
||||
) {
|
||||
proxy.auth = proxy.password;
|
||||
delete proxy.password;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'tfo') &&
|
||||
!isPresent(proxy, 'fast-open')
|
||||
) {
|
||||
proxy['fast-open'] = proxy.tfo;
|
||||
delete proxy.tfo;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'down') &&
|
||||
!isPresent(proxy, 'down-speed')
|
||||
) {
|
||||
proxy['down-speed'] = proxy.down;
|
||||
delete proxy.down;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'up') &&
|
||||
!isPresent(proxy, 'up-speed')
|
||||
) {
|
||||
proxy['up-speed'] = proxy.up;
|
||||
delete proxy.up;
|
||||
}
|
||||
if (isPresent(proxy, 'down-speed')) {
|
||||
proxy['down-speed'] =
|
||||
`${proxy['down-speed']}`.match(/\d+/)?.[0] || 0;
|
||||
}
|
||||
if (isPresent(proxy, 'up-speed')) {
|
||||
proxy['up-speed'] =
|
||||
`${proxy['up-speed']}`.match(/\d+/)?.[0] || 0;
|
||||
}
|
||||
} 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 (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 (
|
||||
['vmess', 'vless'].includes(proxy.type) &&
|
||||
proxy.network === 'http'
|
||||
isPresent(proxy, 'cipher') &&
|
||||
![
|
||||
'auto',
|
||||
'aes-128-gcm',
|
||||
'chacha20-poly1305',
|
||||
'none',
|
||||
].includes(proxy.cipher)
|
||||
) {
|
||||
let httpPath = proxy['http-opts']?.path;
|
||||
if (
|
||||
isPresent(proxy, 'http-opts.path') &&
|
||||
!Array.isArray(httpPath)
|
||||
) {
|
||||
proxy['http-opts'].path = [httpPath];
|
||||
}
|
||||
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||
if (
|
||||
isPresent(proxy, 'http-opts.headers.Host') &&
|
||||
!Array.isArray(httpHost)
|
||||
) {
|
||||
proxy['http-opts'].headers.Host = [httpHost];
|
||||
}
|
||||
proxy.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 (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||
proxy.type,
|
||||
)
|
||||
isPresent(proxy, 'tfo') &&
|
||||
!isPresent(proxy, 'fast-open')
|
||||
) {
|
||||
delete proxy.tls;
|
||||
proxy['fast-open'] = proxy.tfo;
|
||||
delete proxy.tfo;
|
||||
}
|
||||
if (proxy['tls-fingerprint']) {
|
||||
proxy.fingerprint = proxy['tls-fingerprint'];
|
||||
}
|
||||
delete proxy['tls-fingerprint'];
|
||||
|
||||
if (proxy['test-url']) {
|
||||
proxy['benchmark-url'] = proxy['test-url'];
|
||||
delete proxy['test-url'];
|
||||
}
|
||||
|
||||
delete proxy.subName;
|
||||
delete proxy.collectionName;
|
||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
|
||||
if (
|
||||
['grpc'].includes(proxy.network) &&
|
||||
proxy[`${proxy.network}-opts`]
|
||||
(!proxy.token || proxy.token.length === 0) &&
|
||||
!isPresent(proxy, 'version')
|
||||
) {
|
||||
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||
proxy.version = 5;
|
||||
}
|
||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||
})
|
||||
.join('')
|
||||
);
|
||||
} else if (proxy.type === 'hysteria') {
|
||||
// auth_str 将会在未来某个时候删除 但是有的机场不规范
|
||||
if (
|
||||
isPresent(proxy, 'auth_str') &&
|
||||
!isPresent(proxy, 'auth-str')
|
||||
) {
|
||||
proxy['auth-str'] = proxy['auth_str'];
|
||||
}
|
||||
if (isPresent(proxy, 'alpn')) {
|
||||
proxy.alpn = Array.isArray(proxy.alpn)
|
||||
? proxy.alpn
|
||||
: [proxy.alpn];
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'tfo') &&
|
||||
!isPresent(proxy, 'fast-open')
|
||||
) {
|
||||
proxy['fast-open'] = proxy.tfo;
|
||||
delete proxy.tfo;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'down') &&
|
||||
!isPresent(proxy, 'down-speed')
|
||||
) {
|
||||
proxy['down-speed'] = proxy.down;
|
||||
delete proxy.down;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'up') &&
|
||||
!isPresent(proxy, 'up-speed')
|
||||
) {
|
||||
proxy['up-speed'] = proxy.up;
|
||||
delete proxy.up;
|
||||
}
|
||||
if (isPresent(proxy, 'down-speed')) {
|
||||
proxy['down-speed'] =
|
||||
`${proxy['down-speed']}`.match(/\d+/)?.[0] || 0;
|
||||
}
|
||||
if (isPresent(proxy, 'up-speed')) {
|
||||
proxy['up-speed'] =
|
||||
`${proxy['up-speed']}`.match(/\d+/)?.[0] || 0;
|
||||
}
|
||||
} else if (proxy.type === 'hysteria2') {
|
||||
if (
|
||||
isPresent(proxy, 'password') &&
|
||||
!isPresent(proxy, 'auth')
|
||||
) {
|
||||
proxy.auth = proxy.password;
|
||||
delete proxy.password;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'tfo') &&
|
||||
!isPresent(proxy, 'fast-open')
|
||||
) {
|
||||
proxy['fast-open'] = proxy.tfo;
|
||||
delete proxy.tfo;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'down') &&
|
||||
!isPresent(proxy, 'down-speed')
|
||||
) {
|
||||
proxy['down-speed'] = proxy.down;
|
||||
delete proxy.down;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'up') &&
|
||||
!isPresent(proxy, 'up-speed')
|
||||
) {
|
||||
proxy['up-speed'] = proxy.up;
|
||||
delete proxy.up;
|
||||
}
|
||||
if (isPresent(proxy, 'down-speed')) {
|
||||
proxy['down-speed'] =
|
||||
`${proxy['down-speed']}`.match(/\d+/)?.[0] || 0;
|
||||
}
|
||||
if (isPresent(proxy, 'up-speed')) {
|
||||
proxy['up-speed'] =
|
||||
`${proxy['up-speed']}`.match(/\d+/)?.[0] || 0;
|
||||
}
|
||||
} 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 (
|
||||
['vmess', 'vless'].includes(proxy.type) &&
|
||||
proxy.network === 'h2'
|
||||
) {
|
||||
let path = proxy['h2-opts']?.path;
|
||||
if (
|
||||
isPresent(proxy, 'h2-opts.path') &&
|
||||
Array.isArray(path)
|
||||
) {
|
||||
proxy['h2-opts'].path = path[0];
|
||||
}
|
||||
let host = proxy['h2-opts']?.headers?.host;
|
||||
if (
|
||||
isPresent(proxy, 'h2-opts.headers.Host') &&
|
||||
!Array.isArray(host)
|
||||
) {
|
||||
proxy['h2-opts'].headers.host = [host];
|
||||
}
|
||||
}
|
||||
if (proxy['plugin-opts']?.tls) {
|
||||
if (isPresent(proxy, 'skip-cert-verify')) {
|
||||
proxy['plugin-opts']['skip-cert-verify'] =
|
||||
proxy['skip-cert-verify'];
|
||||
}
|
||||
}
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||
proxy.type,
|
||||
)
|
||||
) {
|
||||
delete proxy.tls;
|
||||
}
|
||||
if (proxy['tls-fingerprint']) {
|
||||
proxy.fingerprint = proxy['tls-fingerprint'];
|
||||
}
|
||||
delete proxy['tls-fingerprint'];
|
||||
if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
|
||||
delete proxy.tls;
|
||||
}
|
||||
|
||||
if (proxy['test-url']) {
|
||||
proxy['benchmark-url'] = proxy['test-url'];
|
||||
delete proxy['test-url'];
|
||||
}
|
||||
if (proxy['test-timeout']) {
|
||||
proxy['benchmark-timeout'] = proxy['test-timeout'];
|
||||
delete proxy['test-timeout'];
|
||||
}
|
||||
|
||||
delete proxy.subName;
|
||||
delete proxy.collectionName;
|
||||
if (
|
||||
['grpc'].includes(proxy.network) &&
|
||||
proxy[`${proxy.network}-opts`]
|
||||
) {
|
||||
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||
}
|
||||
return proxy;
|
||||
});
|
||||
return type === 'internal'
|
||||
? list
|
||||
: 'proxies:\n' +
|
||||
list
|
||||
.map((proxy) => ' - ' + JSON.stringify(proxy) + '\n')
|
||||
.join('');
|
||||
};
|
||||
return { type, produce };
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const targetPlatform = 'Surfboard';
|
||||
|
||||
export default function Surfboard_Producer() {
|
||||
const produce = (proxy) => {
|
||||
proxy.name = proxy.name.replace(/=/g, '');
|
||||
switch (proxy.type) {
|
||||
case 'ss':
|
||||
return shadowsocks(proxy);
|
||||
@@ -30,6 +31,32 @@ export default function Surfboard_Producer() {
|
||||
function shadowsocks(proxy) {
|
||||
const result = new Result(proxy);
|
||||
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||
if (
|
||||
![
|
||||
'aes-128-gcm',
|
||||
'aes-192-gcm',
|
||||
'aes-256-gcm',
|
||||
'chacha20-ietf-poly1305',
|
||||
'xchacha20-ietf-poly1305',
|
||||
'rc4',
|
||||
'rc4-md5',
|
||||
'aes-128-cfb',
|
||||
'aes-192-cfb',
|
||||
'aes-256-cfb',
|
||||
'aes-128-ctr',
|
||||
'aes-192-ctr',
|
||||
'aes-256-ctr',
|
||||
'bf-cfb',
|
||||
'camellia-128-cfb',
|
||||
'camellia-192-cfb',
|
||||
'camellia-256-cfb',
|
||||
'salsa20',
|
||||
'chacha20',
|
||||
'chacha20-ietf',
|
||||
].includes(proxy.cipher)
|
||||
) {
|
||||
throw new Error(`cipher ${proxy.cipher} is not supported`);
|
||||
}
|
||||
result.append(`,encrypt-method=${proxy.cipher}`);
|
||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Result, isPresent } from './utils';
|
||||
import { isNotBlank } from '@/utils';
|
||||
import { isNotBlank, getIfNotBlank } from '@/utils';
|
||||
import $ from '@/core/app';
|
||||
|
||||
const targetPlatform = 'Surge';
|
||||
@@ -13,7 +13,7 @@ const ipVersions = {
|
||||
};
|
||||
|
||||
export default function Surge_Producer() {
|
||||
const produce = (proxy) => {
|
||||
const produce = (proxy, type, opts = {}) => {
|
||||
switch (proxy.type) {
|
||||
case 'ss':
|
||||
return shadowsocks(proxy);
|
||||
@@ -30,9 +30,15 @@ export default function Surge_Producer() {
|
||||
case 'tuic':
|
||||
return tuic(proxy);
|
||||
case 'wireguard-surge':
|
||||
return wireguard(proxy);
|
||||
return wireguard_surge(proxy);
|
||||
case 'hysteria2':
|
||||
return hysteria2(proxy);
|
||||
case 'ssh':
|
||||
return ssh(proxy);
|
||||
}
|
||||
|
||||
if (opts['include-unsupported-proxy'] && proxy.type === 'wireguard') {
|
||||
return wireguard(proxy);
|
||||
}
|
||||
throw new Error(
|
||||
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
||||
@@ -44,13 +50,46 @@ export default function Surge_Producer() {
|
||||
function shadowsocks(proxy) {
|
||||
const result = new Result(proxy);
|
||||
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||
if (!proxy.cipher) {
|
||||
proxy.cipher = 'none';
|
||||
}
|
||||
if (
|
||||
![
|
||||
'aes-128-gcm',
|
||||
'aes-192-gcm',
|
||||
'aes-256-gcm',
|
||||
'chacha20-ietf-poly1305',
|
||||
'xchacha20-ietf-poly1305',
|
||||
'rc4',
|
||||
'rc4-md5',
|
||||
'aes-128-cfb',
|
||||
'aes-192-cfb',
|
||||
'aes-256-cfb',
|
||||
'aes-128-ctr',
|
||||
'aes-192-ctr',
|
||||
'aes-256-ctr',
|
||||
'bf-cfb',
|
||||
'camellia-128-cfb',
|
||||
'camellia-192-cfb',
|
||||
'camellia-256-cfb',
|
||||
'cast5-cfb',
|
||||
'des-cfb',
|
||||
'idea-cfb',
|
||||
'rc2-cfb',
|
||||
'seed-cfb',
|
||||
'salsa20',
|
||||
'chacha20',
|
||||
'chacha20-ietf',
|
||||
'none',
|
||||
].includes(proxy.cipher)
|
||||
) {
|
||||
throw new Error(`cipher ${proxy.cipher} is not supported`);
|
||||
}
|
||||
result.append(`,encrypt-method=${proxy.cipher}`);
|
||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
@@ -69,7 +108,7 @@ function shadowsocks(proxy) {
|
||||
`,obfs-uri=${proxy['plugin-opts'].path}`,
|
||||
'plugin-opts.path',
|
||||
);
|
||||
} else {
|
||||
} else if (!['shadow-tls'].includes(proxy.plugin)) {
|
||||
throw new Error(`plugin ${proxy.plugin} is not supported`);
|
||||
}
|
||||
}
|
||||
@@ -82,6 +121,21 @@ function shadowsocks(proxy) {
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
result.appendIfPresent(
|
||||
`,test-timeout=${proxy['test-timeout']}`,
|
||||
'test-timeout',
|
||||
);
|
||||
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||
result.appendIfPresent(
|
||||
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||
'allow-other-interface',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,interface=${proxy['interface-name']}`,
|
||||
'interface-name',
|
||||
);
|
||||
|
||||
// shadow-tls
|
||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||
@@ -95,6 +149,24 @@ function shadowsocks(proxy) {
|
||||
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
|
||||
'shadow-tls-sni',
|
||||
);
|
||||
} else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {
|
||||
const password = proxy['plugin-opts'].password;
|
||||
const host = proxy['plugin-opts'].host;
|
||||
const version = proxy['plugin-opts'].version;
|
||||
if (password) {
|
||||
result.append(`,shadow-tls-password=${password}`);
|
||||
if (host) {
|
||||
result.append(`,shadow-tls-sni=${host}`);
|
||||
}
|
||||
if (version) {
|
||||
if (version < 2) {
|
||||
throw new Error(
|
||||
`shadow-tls version ${version} is not supported`,
|
||||
);
|
||||
}
|
||||
result.append(`,shadow-tls-version=${version}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// block-quic
|
||||
@@ -114,10 +186,8 @@ function trojan(proxy) {
|
||||
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
@@ -151,6 +221,21 @@ function trojan(proxy) {
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
result.appendIfPresent(
|
||||
`,test-timeout=${proxy['test-timeout']}`,
|
||||
'test-timeout',
|
||||
);
|
||||
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||
result.appendIfPresent(
|
||||
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||
'allow-other-interface',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,interface=${proxy['interface-name']}`,
|
||||
'interface-name',
|
||||
);
|
||||
|
||||
// shadow-tls
|
||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||
@@ -183,10 +268,8 @@ function vmess(proxy) {
|
||||
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||
result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
@@ -227,6 +310,21 @@ function vmess(proxy) {
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
result.appendIfPresent(
|
||||
`,test-timeout=${proxy['test-timeout']}`,
|
||||
'test-timeout',
|
||||
);
|
||||
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||
result.appendIfPresent(
|
||||
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||
'allow-other-interface',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,interface=${proxy['interface-name']}`,
|
||||
'interface-name',
|
||||
);
|
||||
|
||||
// shadow-tls
|
||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||
@@ -254,6 +352,64 @@ function vmess(proxy) {
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function ssh(proxy) {
|
||||
const result = new Result(proxy);
|
||||
result.append(`${proxy.name}=ssh,${proxy.server},${proxy.port}`);
|
||||
result.appendIfPresent(`,${proxy.username}`, 'username');
|
||||
result.appendIfPresent(`,${proxy.password}`, 'password');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,idle-timeout=${proxy['idle-timeout']}`,
|
||||
'idle-timeout',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,server-fingerprint="${proxy['server-fingerprint']}"`,
|
||||
'server-fingerprint',
|
||||
);
|
||||
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
'no-error-alert',
|
||||
);
|
||||
|
||||
// tfo
|
||||
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
result.appendIfPresent(
|
||||
`,test-timeout=${proxy['test-timeout']}`,
|
||||
'test-timeout',
|
||||
);
|
||||
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||
result.appendIfPresent(
|
||||
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||
'allow-other-interface',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,interface=${proxy['interface-name']}`,
|
||||
'interface-name',
|
||||
);
|
||||
|
||||
// block-quic
|
||||
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
|
||||
|
||||
// underlying-proxy
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
function http(proxy) {
|
||||
const result = new Result(proxy);
|
||||
const type = proxy.tls ? 'https' : 'http';
|
||||
@@ -261,10 +417,8 @@ function http(proxy) {
|
||||
result.appendIfPresent(`,${proxy.username}`, 'username');
|
||||
result.appendIfPresent(`,${proxy.password}`, 'password');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
@@ -292,6 +446,21 @@ function http(proxy) {
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
result.appendIfPresent(
|
||||
`,test-timeout=${proxy['test-timeout']}`,
|
||||
'test-timeout',
|
||||
);
|
||||
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||
result.appendIfPresent(
|
||||
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||
'allow-other-interface',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,interface=${proxy['interface-name']}`,
|
||||
'interface-name',
|
||||
);
|
||||
|
||||
// shadow-tls
|
||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||
@@ -326,10 +495,8 @@ function socks5(proxy) {
|
||||
result.appendIfPresent(`,${proxy.username}`, 'username');
|
||||
result.appendIfPresent(`,${proxy.password}`, 'password');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
@@ -359,6 +526,21 @@ function socks5(proxy) {
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
result.appendIfPresent(
|
||||
`,test-timeout=${proxy['test-timeout']}`,
|
||||
'test-timeout',
|
||||
);
|
||||
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||
result.appendIfPresent(
|
||||
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||
'allow-other-interface',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,interface=${proxy['interface-name']}`,
|
||||
'interface-name',
|
||||
);
|
||||
|
||||
// shadow-tls
|
||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||
@@ -392,10 +574,8 @@ function snell(proxy) {
|
||||
result.appendIfPresent(`,version=${proxy.version}`, 'version');
|
||||
result.appendIfPresent(`,psk=${proxy.psk}`, 'psk');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
@@ -424,6 +604,21 @@ function snell(proxy) {
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
result.appendIfPresent(
|
||||
`,test-timeout=${proxy['test-timeout']}`,
|
||||
'test-timeout',
|
||||
);
|
||||
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||
result.appendIfPresent(
|
||||
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||
'allow-other-interface',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,interface=${proxy['interface-name']}`,
|
||||
'interface-name',
|
||||
);
|
||||
|
||||
// shadow-tls
|
||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||
@@ -472,10 +667,8 @@ function tuic(proxy) {
|
||||
'alpn',
|
||||
);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
@@ -504,6 +697,21 @@ function tuic(proxy) {
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
result.appendIfPresent(
|
||||
`,test-timeout=${proxy['test-timeout']}`,
|
||||
'test-timeout',
|
||||
);
|
||||
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||
result.appendIfPresent(
|
||||
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||
'allow-other-interface',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,interface=${proxy['interface-name']}`,
|
||||
'interface-name',
|
||||
);
|
||||
|
||||
// shadow-tls
|
||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||
@@ -534,6 +742,122 @@ function tuic(proxy) {
|
||||
}
|
||||
|
||||
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'];
|
||||
// https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717
|
||||
proxy['allowed-ips'] = proxy.peers[0]['allowed-ips'];
|
||||
proxy.reserved = proxy.peers[0].reserved;
|
||||
}
|
||||
const result = new Result(proxy);
|
||||
|
||||
result.append(`# WireGuard Proxy ${proxy.name}
|
||||
${proxy.name}=wireguard`);
|
||||
|
||||
proxy['section-name'] = getIfNotBlank(proxy['section-name'], proxy.name);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,section-name=${proxy['section-name']}`,
|
||||
'section-name',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
'no-error-alert',
|
||||
);
|
||||
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
result.appendIfPresent(
|
||||
`,test-timeout=${proxy['test-timeout']}`,
|
||||
'test-timeout',
|
||||
);
|
||||
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||
result.appendIfPresent(
|
||||
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||
'allow-other-interface',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,interface=${proxy['interface-name']}`,
|
||||
'interface-name',
|
||||
);
|
||||
|
||||
// shadow-tls
|
||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
|
||||
'shadow-tls-version',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
|
||||
'shadow-tls-sni',
|
||||
);
|
||||
}
|
||||
|
||||
// block-quic
|
||||
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
|
||||
|
||||
// underlying-proxy
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
result.append(`
|
||||
# WireGuard Section ${proxy.name}
|
||||
[WireGuard ${proxy['section-name']}]
|
||||
private-key = ${proxy['private-key']}`);
|
||||
|
||||
result.appendIfPresent(`\nself-ip = ${proxy.ip}`, 'ip');
|
||||
result.appendIfPresent(`\nself-ip-v6 = ${proxy.ipv6}`, 'ipv6');
|
||||
if (proxy.dns) {
|
||||
if (Array.isArray(proxy.dns)) {
|
||||
proxy.dns = proxy.dns.join(', ');
|
||||
}
|
||||
result.append(`\ndns-server = ${proxy.dns}`);
|
||||
}
|
||||
result.appendIfPresent(`\nmtu = ${proxy.mtu}`, 'mtu');
|
||||
|
||||
if (ip_version === 'prefer-v6') {
|
||||
result.append(`\nprefer-ipv6 = true`);
|
||||
}
|
||||
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;
|
||||
let presharedKey = proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
||||
if (presharedKey) {
|
||||
presharedKey = `,preshared-key="${presharedKey}"`;
|
||||
}
|
||||
const peer = {
|
||||
'public-key': proxy['public-key'],
|
||||
'allowed-ips': allowedIps,
|
||||
endpoint: `${proxy.server}:${proxy.port}`,
|
||||
keepalive: proxy['persistent-keepalive'] || proxy.keepalive,
|
||||
'client-id': reserved,
|
||||
'preshared-key': presharedKey,
|
||||
};
|
||||
result.append(
|
||||
`\npeer = (${Object.keys(peer)
|
||||
.filter((k) => peer[k] != null)
|
||||
.map((k) => `${k} = ${peer[k]}`)
|
||||
.join(', ')})`,
|
||||
);
|
||||
return result.toString();
|
||||
}
|
||||
function wireguard_surge(proxy) {
|
||||
const result = new Result(proxy);
|
||||
|
||||
result.append(`${proxy.name}=wireguard`);
|
||||
@@ -547,13 +871,26 @@ function wireguard(proxy) {
|
||||
'no-error-alert',
|
||||
);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
result.appendIfPresent(
|
||||
`,test-timeout=${proxy['test-timeout']}`,
|
||||
'test-timeout',
|
||||
);
|
||||
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||
result.appendIfPresent(
|
||||
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||
'allow-other-interface',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,interface=${proxy['interface-name']}`,
|
||||
'interface-name',
|
||||
);
|
||||
|
||||
// shadow-tls
|
||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||
@@ -590,10 +927,8 @@ function hysteria2(proxy) {
|
||||
|
||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
@@ -620,6 +955,21 @@ function hysteria2(proxy) {
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
result.appendIfPresent(
|
||||
`,test-timeout=${proxy['test-timeout']}`,
|
||||
'test-timeout',
|
||||
);
|
||||
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||
result.appendIfPresent(
|
||||
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||
'allow-other-interface',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,interface=${proxy['interface-name']}`,
|
||||
'interface-name',
|
||||
);
|
||||
|
||||
// shadow-tls
|
||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { Result } from './utils';
|
||||
import Surge_Producer from './surge';
|
||||
import { isIPv4, isIPv6, isPresent } from '@/utils';
|
||||
import $ from '@/core/app';
|
||||
|
||||
// const targetPlatform = 'SurgeMac';
|
||||
const targetPlatform = 'SurgeMac';
|
||||
|
||||
const surge_Producer = Surge_Producer();
|
||||
|
||||
export default function SurgeMac_Producer() {
|
||||
const produce = (proxy) => {
|
||||
switch (proxy.type) {
|
||||
case 'external':
|
||||
return external(proxy);
|
||||
case 'ssr':
|
||||
return shadowsocksr(proxy);
|
||||
default:
|
||||
@@ -16,19 +20,67 @@ export default function SurgeMac_Producer() {
|
||||
};
|
||||
return { produce };
|
||||
}
|
||||
|
||||
function shadowsocksr(proxy) {
|
||||
function external(proxy) {
|
||||
const result = new Result(proxy);
|
||||
|
||||
proxy.local_port = '__SubStoreLocalPort__';
|
||||
proxy.local_address = proxy.local_address ?? '127.0.0.1';
|
||||
|
||||
if (!proxy.exec || !proxy['local-port']) {
|
||||
throw new Error(`${proxy.type}: exec and local-port are required`);
|
||||
}
|
||||
result.append(
|
||||
`${proxy.name} = external, exec = "${
|
||||
proxy.exec || '/usr/local/bin/ssr-local'
|
||||
}", address = "${proxy.server}", local-port = ${proxy.local_port}`,
|
||||
`${proxy.name}=external,exec="${proxy.exec}",local-port=${proxy['local-port']}`,
|
||||
);
|
||||
|
||||
if (Array.isArray(proxy.args)) {
|
||||
proxy.args.map((args) => {
|
||||
result.append(`,args="${args}"`);
|
||||
});
|
||||
}
|
||||
if (Array.isArray(proxy.addresses)) {
|
||||
proxy.addresses.map((addresses) => {
|
||||
result.append(`,addresses=${addresses}`);
|
||||
});
|
||||
}
|
||||
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
'no-error-alert',
|
||||
);
|
||||
|
||||
// tfo
|
||||
if (isPresent(proxy, 'tfo')) {
|
||||
result.append(`,tfo=${proxy['tfo']}`);
|
||||
} else if (isPresent(proxy, 'fast-open')) {
|
||||
result.append(`,tfo=${proxy['fast-open']}`);
|
||||
}
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// block-quic
|
||||
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
function shadowsocksr(proxy) {
|
||||
const external_proxy = {
|
||||
...proxy,
|
||||
type: 'external',
|
||||
exec: proxy.exec || '/usr/local/bin/ssr-local',
|
||||
'local-port': '__SubStoreLocalPort__',
|
||||
args: [],
|
||||
addresses: [],
|
||||
'local-address':
|
||||
proxy.local_address ?? proxy['local-address'] ?? '127.0.0.1',
|
||||
};
|
||||
|
||||
// https://manual.nssurge.com/policy/external-proxy.html
|
||||
if (isIP(proxy.server)) {
|
||||
external_proxy.addresses.push(proxy.server);
|
||||
} else {
|
||||
$.log(
|
||||
`Platform ${targetPlatform}, proxy type ${proxy.type}: addresses should be an IP address, but got ${proxy.server}`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries({
|
||||
cipher: '-m',
|
||||
obfs: '-o',
|
||||
@@ -37,14 +89,16 @@ function shadowsocksr(proxy) {
|
||||
protocol: '-O',
|
||||
'protocol-param': '-G',
|
||||
server: '-s',
|
||||
local_port: '-l',
|
||||
local_address: '-b',
|
||||
'local-port': '-l',
|
||||
'local-address': '-b',
|
||||
})) {
|
||||
result.appendIfPresent(
|
||||
`, args = "${value}", args = "${proxy[key]}"`,
|
||||
key,
|
||||
);
|
||||
external_proxy.args.push(value);
|
||||
external_proxy.args.push(external_proxy[key]);
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
return external(external_proxy);
|
||||
}
|
||||
|
||||
function isIP(ip) {
|
||||
return isIPv4(ip) || isIPv6(ip);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,11 @@ export default function URI_Producer() {
|
||||
const type = 'SINGLE';
|
||||
const produce = (proxy) => {
|
||||
let result = '';
|
||||
delete proxy.subName;
|
||||
delete proxy.collectionName;
|
||||
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
|
||||
delete proxy.tls;
|
||||
}
|
||||
if (proxy.server && isIPv6(proxy.server)) {
|
||||
proxy.server = `[${proxy.server}]`;
|
||||
}
|
||||
@@ -39,6 +44,12 @@ export default function URI_Producer() {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (proxy['udp-over-tcp']) {
|
||||
result = `${result}${proxy.plugin ? '&' : '?'}uot=1`;
|
||||
}
|
||||
if (proxy.tfo) {
|
||||
result = `${result}${proxy.plugin ? '&' : '?'}tfo=1`;
|
||||
}
|
||||
result += `#${encodeURIComponent(proxy.name)}`;
|
||||
break;
|
||||
case 'ssr':
|
||||
@@ -191,9 +202,9 @@ export default function URI_Producer() {
|
||||
|
||||
result = `vless://${proxy.uuid}@${proxy.server}:${
|
||||
proxy.port
|
||||
}?${vlessTransport}&security=${encodeURIComponent(
|
||||
}?security=${encodeURIComponent(
|
||||
security,
|
||||
)}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${pbk}#${encodeURIComponent(
|
||||
)}${vlessTransport}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${pbk}#${encodeURIComponent(
|
||||
proxy.name,
|
||||
)}`;
|
||||
break;
|
||||
@@ -279,6 +290,119 @@ export default function URI_Producer() {
|
||||
'&',
|
||||
)}#${encodeURIComponent(proxy.name)}`;
|
||||
break;
|
||||
case 'hysteria':
|
||||
let hysteriaParams = [];
|
||||
Object.keys(proxy).forEach((key) => {
|
||||
if (!['name', 'type', 'server', 'port'].includes(key)) {
|
||||
const i = key.replace(/-/, '_');
|
||||
if (['alpn'].includes(key)) {
|
||||
if (proxy[key]) {
|
||||
hysteriaParams.push(
|
||||
`${i}=${encodeURIComponent(
|
||||
Array.isArray(proxy[key])
|
||||
? proxy[key][0]
|
||||
: proxy[key],
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
} else if (['skip-cert-verify'].includes(key)) {
|
||||
if (proxy[key]) {
|
||||
hysteriaParams.push(`insecure=1`);
|
||||
}
|
||||
} else if (['tfo', 'fast-open'].includes(key)) {
|
||||
if (
|
||||
proxy[key] &&
|
||||
!hysteriaParams.includes('fastopen=1')
|
||||
) {
|
||||
hysteriaParams.push(`fastopen=1`);
|
||||
}
|
||||
} else if (['ports'].includes(key)) {
|
||||
hysteriaParams.push(`mport=${proxy[key]}`);
|
||||
} else if (['auth-str'].includes(key)) {
|
||||
hysteriaParams.push(`auth=${proxy[key]}`);
|
||||
} else if (['up'].includes(key)) {
|
||||
hysteriaParams.push(`upmbps=${proxy[key]}`);
|
||||
} else if (['down'].includes(key)) {
|
||||
hysteriaParams.push(`downmbps=${proxy[key]}`);
|
||||
} else if (['_obfs'].includes(key)) {
|
||||
hysteriaParams.push(`obfs=${proxy[key]}`);
|
||||
} else if (['obfs'].includes(key)) {
|
||||
hysteriaParams.push(`obfsParam=${proxy[key]}`);
|
||||
} else if (['sni'].includes(key)) {
|
||||
hysteriaParams.push(`peer=${proxy[key]}`);
|
||||
} else if (proxy[key]) {
|
||||
hysteriaParams.push(
|
||||
`${i}=${encodeURIComponent(proxy[key])}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
result = `hysteria://${proxy.server}:${
|
||||
proxy.port
|
||||
}?${hysteriaParams.join('&')}#${encodeURIComponent(
|
||||
proxy.name,
|
||||
)}`;
|
||||
break;
|
||||
|
||||
case 'tuic':
|
||||
if (!proxy.token || proxy.token.length === 0) {
|
||||
let tuicParams = [];
|
||||
Object.keys(proxy).forEach((key) => {
|
||||
if (
|
||||
![
|
||||
'name',
|
||||
'type',
|
||||
'uuid',
|
||||
'password',
|
||||
'server',
|
||||
'port',
|
||||
].includes(key)
|
||||
) {
|
||||
const i = key.replace(/-/, '_');
|
||||
if (['alpn'].includes(key)) {
|
||||
if (proxy[key]) {
|
||||
tuicParams.push(
|
||||
`${i}=${encodeURIComponent(
|
||||
Array.isArray(proxy[key])
|
||||
? proxy[key][0]
|
||||
: proxy[key],
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
} else if (['skip-cert-verify'].includes(key)) {
|
||||
if (proxy[key]) {
|
||||
tuicParams.push(`allow_insecure=1`);
|
||||
}
|
||||
} else if (['tfo', 'fast-open'].includes(key)) {
|
||||
if (
|
||||
proxy[key] &&
|
||||
!tuicParams.includes('fast_open=1')
|
||||
) {
|
||||
tuicParams.push(`fast_open=1`);
|
||||
}
|
||||
} else if (
|
||||
['disable-sni', 'reduce-rtt'].includes(key) &&
|
||||
proxy[key]
|
||||
) {
|
||||
tuicParams.push(`${i}=1`);
|
||||
} else if (proxy[key]) {
|
||||
tuicParams.push(
|
||||
`${i}=${encodeURIComponent(proxy[key])}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
result = `tuic://${encodeURIComponent(
|
||||
proxy.uuid,
|
||||
)}:${encodeURIComponent(proxy.password)}@${proxy.server}:${
|
||||
proxy.port
|
||||
}?${tuicParams.join('&')}#${encodeURIComponent(
|
||||
proxy.name,
|
||||
)}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ const RULE_TYPES_MAPPING = [
|
||||
[/^(IN|SRC)-PORT$/, 'IN-PORT'],
|
||||
[/^PROTOCOL$/, 'PROTOCOL'],
|
||||
[/^IP-CIDR$/i, 'IP-CIDR'],
|
||||
[/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/],
|
||||
[/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/, 'IP-CIDR6'],
|
||||
];
|
||||
|
||||
function AllRuleParser() {
|
||||
|
||||
@@ -8,7 +8,7 @@ function HTML() {
|
||||
|
||||
function ClashProvider() {
|
||||
const name = 'Clash Provider';
|
||||
const test = (raw) => raw.indexOf('payload:') === 0;
|
||||
const test = (raw) => /^payload:/gm.exec(raw).index >= 0;
|
||||
const parse = (raw) => {
|
||||
return raw.replace('payload:', '').replace(/^\s*-\s*/gm, '');
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import YAML from 'static-js-yaml';
|
||||
import YAML from '@/utils/yaml';
|
||||
|
||||
function QXFilter() {
|
||||
const type = 'SINGLE';
|
||||
@@ -30,8 +30,9 @@ function SurgeRuleSet() {
|
||||
const type = 'SINGLE';
|
||||
const func = (rule) => {
|
||||
let output = `${rule.type},${rule.content}`;
|
||||
if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') {
|
||||
output += rule.options ? `,${rule.options[0]}` : '';
|
||||
if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type)) {
|
||||
output +=
|
||||
rule.options?.length > 0 ? `,${rule.options.join(',')}` : '';
|
||||
}
|
||||
return output;
|
||||
};
|
||||
@@ -44,6 +45,12 @@ function LoonRules() {
|
||||
// skip unsupported rules
|
||||
const UNSUPPORTED = ['DEST-PORT', 'SRC-IP', 'IN-PORT', 'PROTOCOL'];
|
||||
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
|
||||
if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type) && rule.options) {
|
||||
// Loon only supports the no-resolve option
|
||||
rule.options = rule.options.filter((option) =>
|
||||
['no-resolve'].includes(option),
|
||||
);
|
||||
}
|
||||
return SurgeRuleSet().func(rule);
|
||||
};
|
||||
return { type, func };
|
||||
@@ -62,8 +69,17 @@ function ClashRuleProvider() {
|
||||
let output = `${TRANSFORM[rule.type] || rule.type},${
|
||||
rule.content
|
||||
}`;
|
||||
if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') {
|
||||
output += rule.options ? `,${rule.options[0]}` : '';
|
||||
if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type)) {
|
||||
if (rule.options) {
|
||||
// Clash only supports the no-resolve option
|
||||
rule.options = rule.options.filter((option) =>
|
||||
['no-resolve'].includes(option),
|
||||
);
|
||||
}
|
||||
output +=
|
||||
rule.options?.length > 0
|
||||
? `,${rule.options.join(',')}`
|
||||
: '';
|
||||
}
|
||||
return output;
|
||||
}),
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { version } from '../../package.json';
|
||||
import { SETTINGS_KEY, ARTIFACTS_KEY } from '@/constants';
|
||||
import {
|
||||
SETTINGS_KEY,
|
||||
ARTIFACTS_KEY,
|
||||
SUBS_KEY,
|
||||
COLLECTIONS_KEY,
|
||||
} from '@/constants';
|
||||
import $ from '@/core/app';
|
||||
import { produceArtifact } from '@/restful/sync';
|
||||
import { syncToGist } from '@/restful/artifacts';
|
||||
import { findByName } from '@/utils/database';
|
||||
|
||||
!(async function () {
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
@@ -30,23 +36,83 @@ async function doSync() {
|
||||
const files = {};
|
||||
|
||||
try {
|
||||
const invalid = [];
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
const subNames = [];
|
||||
allArtifacts.map((artifact) => {
|
||||
if (artifact.sync && artifact.source) {
|
||||
if (artifact.type === 'subscription') {
|
||||
const subName = artifact.source;
|
||||
const sub = findByName(allSubs, subName);
|
||||
if (sub && sub.url && !subNames.includes(subName)) {
|
||||
subNames.push(subName);
|
||||
}
|
||||
} else if (artifact.type === 'collection') {
|
||||
const collection = findByName(allCols, artifact.source);
|
||||
if (collection && Array.isArray(collection.subscriptions)) {
|
||||
collection.subscriptions.map((subName) => {
|
||||
const sub = findByName(allSubs, subName);
|
||||
if (sub && sub.url && !subNames.includes(subName)) {
|
||||
subNames.push(subName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (subNames.length > 0) {
|
||||
await Promise.all(
|
||||
subNames.map(async (subName) => {
|
||||
try {
|
||||
await produceArtifact({
|
||||
type: 'subscription',
|
||||
name: subName,
|
||||
});
|
||||
} catch (e) {
|
||||
// $.error(`${e.message ?? e}`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
await Promise.all(
|
||||
allArtifacts.map(async (artifact) => {
|
||||
if (artifact.sync) {
|
||||
$.info(`正在同步云配置:${artifact.name}...`);
|
||||
const output = await produceArtifact({
|
||||
type: artifact.type,
|
||||
name: artifact.source,
|
||||
platform: artifact.platform,
|
||||
});
|
||||
try {
|
||||
if (artifact.sync && artifact.source) {
|
||||
$.info(`正在同步云配置:${artifact.name}...`);
|
||||
const output = await produceArtifact({
|
||||
type: artifact.type,
|
||||
name: artifact.source,
|
||||
platform: artifact.platform,
|
||||
produceOpts: {
|
||||
'include-unsupported-proxy':
|
||||
artifact.includeUnsupportedProxy,
|
||||
},
|
||||
});
|
||||
|
||||
files[artifact.name] = {
|
||||
content: output,
|
||||
};
|
||||
// if (!output || output.length === 0)
|
||||
// throw new Error('该配置的结果为空 不进行上传');
|
||||
|
||||
files[encodeURIComponent(artifact.name)] = {
|
||||
content: output,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`同步配置 ${artifact.name} 发生错误: ${e.message ?? e}`,
|
||||
);
|
||||
invalid.push(artifact.name);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (invalid.length > 0) {
|
||||
throw new Error(
|
||||
`同步配置 ${invalid.join(', ')} 发生错误 详情请查看日志`,
|
||||
);
|
||||
}
|
||||
|
||||
const resp = await syncToGist(files);
|
||||
const body = JSON.parse(resp.body);
|
||||
|
||||
@@ -54,17 +120,25 @@ async function doSync() {
|
||||
if (artifact.sync) {
|
||||
artifact.updated = new Date().getTime();
|
||||
// extract real url from gist
|
||||
artifact.url = body.files[artifact.name].raw_url.replace(
|
||||
/\/raw\/[^/]*\/(.*)/,
|
||||
'/raw/$1',
|
||||
);
|
||||
let files = body.files;
|
||||
let isGitLab;
|
||||
if (Array.isArray(files)) {
|
||||
isGitLab = true;
|
||||
files = Object.fromEntries(
|
||||
files.map((item) => [item.path, item]),
|
||||
);
|
||||
}
|
||||
const url = files[encodeURIComponent(artifact.name)]?.raw_url;
|
||||
artifact.url = isGitLab
|
||||
? url
|
||||
: url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
|
||||
}
|
||||
}
|
||||
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
$.notify('🌍 Sub-Store', '全部订阅同步成功!');
|
||||
} catch (err) {
|
||||
$.notify('🌍 Sub-Store', '同步订阅失败', `原因:${err}`);
|
||||
$.error(`无法同步订阅配置到 Gist,原因:${err}`);
|
||||
} catch (e) {
|
||||
$.notify('🌍 Sub-Store', '同步订阅失败', `原因:${e.message ?? e}`);
|
||||
$.error(`无法同步订阅配置到 Gist,原因:${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,28 +2,79 @@
|
||||
import { ProxyUtils } from '@/core/proxy-utils';
|
||||
import { RuleUtils } from '@/core/rule-utils';
|
||||
import { version } from '../../package.json';
|
||||
import download from '@/utils/download';
|
||||
|
||||
console.log(
|
||||
`
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
Sub-Store -- v${version}
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
`,
|
||||
);
|
||||
let result = '';
|
||||
let resource = typeof $resource !== 'undefined' ? $resource : '';
|
||||
let resourceType = typeof $resourceType !== 'undefined' ? $resourceType : '';
|
||||
let resourceUrl = typeof $resourceUrl !== 'undefined' ? $resourceUrl : '';
|
||||
|
||||
const RESOURCE_TYPE = {
|
||||
PROXY: 1,
|
||||
RULE: 2,
|
||||
};
|
||||
!(async () => {
|
||||
console.log(
|
||||
`
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
Sub-Store -- v${version}
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
`,
|
||||
);
|
||||
|
||||
let result = $resource;
|
||||
let arg;
|
||||
if (typeof $argument != 'undefined') {
|
||||
arg = Object.fromEntries(
|
||||
$argument.split('&').map((item) => item.split('=')),
|
||||
);
|
||||
} else {
|
||||
arg = {};
|
||||
}
|
||||
|
||||
if ($resourceType === RESOURCE_TYPE.PROXY) {
|
||||
const proxies = ProxyUtils.parse($resource);
|
||||
result = ProxyUtils.produce(proxies, 'Loon');
|
||||
} else if ($resourceType === RESOURCE_TYPE.RULE) {
|
||||
const rules = RuleUtils.parse($resource);
|
||||
result = RuleUtils.produce(rules, 'Loon');
|
||||
}
|
||||
const RESOURCE_TYPE = {
|
||||
PROXY: 1,
|
||||
RULE: 2,
|
||||
};
|
||||
|
||||
$done(result);
|
||||
result = resource;
|
||||
|
||||
if (resourceType === RESOURCE_TYPE.PROXY) {
|
||||
try {
|
||||
let proxies = ProxyUtils.parse(resource);
|
||||
result = ProxyUtils.produce(proxies, 'Loon');
|
||||
} catch (e) {
|
||||
console.log('解析器: 使用 resource 出现错误');
|
||||
console.log(e.message ?? e);
|
||||
}
|
||||
if ((!result || /^\s*$/.test(result)) && resourceUrl) {
|
||||
console.log(`解析器: 尝试从 ${resourceUrl} 获取订阅`);
|
||||
try {
|
||||
let raw = await download(resourceUrl, arg?.ua, arg?.timeout);
|
||||
let proxies = ProxyUtils.parse(raw);
|
||||
result = ProxyUtils.produce(proxies, 'Loon');
|
||||
} catch (e) {
|
||||
console.log(e.message ?? e);
|
||||
}
|
||||
}
|
||||
} else if (resourceType === RESOURCE_TYPE.RULE) {
|
||||
try {
|
||||
const rules = RuleUtils.parse(resource);
|
||||
result = RuleUtils.produce(rules, 'Loon');
|
||||
} catch (e) {
|
||||
console.log(e.message ?? e);
|
||||
}
|
||||
if ((!result || /^\s*$/.test(result)) && resourceUrl) {
|
||||
console.log(`解析器: 尝试从 ${resourceUrl} 获取规则`);
|
||||
try {
|
||||
let raw = await download(resourceUrl, arg?.ua, arg?.timeout);
|
||||
let rules = RuleUtils.parse(raw);
|
||||
result = RuleUtils.produce(rules, 'Loon');
|
||||
} catch (e) {
|
||||
console.log(e.message ?? e);
|
||||
}
|
||||
}
|
||||
}
|
||||
})()
|
||||
.catch(async (e) => {
|
||||
console.log('解析器: 出现错误');
|
||||
console.log(e.message ?? e);
|
||||
})
|
||||
.finally(() => {
|
||||
$done(result || '');
|
||||
});
|
||||
|
||||
@@ -19,6 +19,8 @@ export default function register($app) {
|
||||
if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);
|
||||
|
||||
// RESTful APIs
|
||||
$app.get('/api/artifacts/restore', restoreArtifacts);
|
||||
|
||||
$app.route('/api/artifacts')
|
||||
.get(getAllArtifacts)
|
||||
.post(createArtifact)
|
||||
@@ -30,6 +32,73 @@ export default function register($app) {
|
||||
.delete(deleteArtifact);
|
||||
}
|
||||
|
||||
async function restoreArtifacts(_, res) {
|
||||
$.info('开始恢复远程配置...');
|
||||
try {
|
||||
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
|
||||
if (!gistToken) {
|
||||
return Promise.reject('未设置 GitHub Token!');
|
||||
}
|
||||
const manager = new Gist({
|
||||
token: gistToken,
|
||||
key: ARTIFACT_REPOSITORY_KEY,
|
||||
syncPlatform,
|
||||
});
|
||||
|
||||
try {
|
||||
const gist = await manager.locate();
|
||||
if (!gist?.files) {
|
||||
throw new Error(`找不到 Sub-Store Gist 文件列表`);
|
||||
}
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
const failed = [];
|
||||
Object.keys(gist.files).map((key) => {
|
||||
const filename = gist.files[key]?.filename;
|
||||
if (filename) {
|
||||
if (encodeURIComponent(filename) !== filename) {
|
||||
$.error(`文件名 ${filename} 未编码 不保存`);
|
||||
failed.push(filename);
|
||||
} else {
|
||||
const artifact = findByName(allArtifacts, filename);
|
||||
if (artifact) {
|
||||
updateByName(allArtifacts, filename, {
|
||||
...artifact,
|
||||
url: gist.files[key]?.raw_url.replace(
|
||||
/\/raw\/[^/]*\/(.*)/,
|
||||
'/raw/$1',
|
||||
),
|
||||
});
|
||||
} else {
|
||||
allArtifacts.push({
|
||||
name: `${filename}`,
|
||||
url: gist.files[key]?.raw_url.replace(
|
||||
/\/raw\/[^/]*\/(.*)/,
|
||||
'/raw/$1',
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
} catch (err) {
|
||||
$.error(`查找 Sub-Store Gist 时发生错误: ${err.message ?? err}`);
|
||||
throw err;
|
||||
}
|
||||
success(res);
|
||||
} catch (e) {
|
||||
$.error(`恢复远程配置失败,原因:${e.message ?? e}`);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
`FAILED_TO_RESTORE_ARTIFACTS`,
|
||||
`Failed to restore artifacts`,
|
||||
`Reason: ${e.message ?? e}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getAllArtifacts(req, res) {
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
success(res, allArtifacts);
|
||||
@@ -140,6 +209,12 @@ async function deleteArtifact(req, res) {
|
||||
files[encodeURIComponent(artifact.name)] = {
|
||||
content: '',
|
||||
};
|
||||
if (encodeURIComponent(artifact.name) !== artifact.name) {
|
||||
files[artifact.name] = {
|
||||
content: '',
|
||||
};
|
||||
}
|
||||
|
||||
// 当别的Sub 删了同步订阅 或 gist里面删了 当前设备没有删除 时 无法删除的bug
|
||||
try {
|
||||
await syncToGist(files);
|
||||
@@ -169,13 +244,14 @@ function validateArtifactName(name) {
|
||||
}
|
||||
|
||||
async function syncToGist(files) {
|
||||
const { gistToken } = $.read(SETTINGS_KEY);
|
||||
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
|
||||
if (!gistToken) {
|
||||
return Promise.reject('未设置Gist Token!');
|
||||
return Promise.reject('未设置 GitHub Token!');
|
||||
}
|
||||
const manager = new Gist({
|
||||
token: gistToken,
|
||||
key: ARTIFACT_REPOSITORY_KEY,
|
||||
syncPlatform,
|
||||
});
|
||||
return manager.upload(files);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import registerPreviewRoutes from './preview';
|
||||
import registerSortingRoutes from './sort';
|
||||
import registerMiscRoutes from './miscs';
|
||||
import registerNodeInfoRoutes from './node-info';
|
||||
import registerParserRoutes from './parser';
|
||||
|
||||
export default function serve() {
|
||||
let port;
|
||||
@@ -38,6 +39,7 @@ export default function serve() {
|
||||
registerSyncRoutes($app);
|
||||
registerNodeInfoRoutes($app);
|
||||
registerMiscRoutes($app);
|
||||
registerParserRoutes($app);
|
||||
|
||||
$app.start();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import $ from '@/core/app';
|
||||
import { ENV } from '@/vendor/open-api';
|
||||
import { failed, success } from '@/restful/response';
|
||||
import { updateArtifactStore, updateGitHubAvatar } from '@/restful/settings';
|
||||
import { updateArtifactStore, updateAvatar } from '@/restful/settings';
|
||||
import resourceCache from '@/utils/resource-cache';
|
||||
import {
|
||||
GIST_BACKUP_FILE_NAME,
|
||||
@@ -68,7 +68,7 @@ function getEnv(req, res) {
|
||||
|
||||
async function refresh(_, res) {
|
||||
// 1. get GitHub avatar and artifact store
|
||||
await updateGitHubAvatar();
|
||||
await updateAvatar();
|
||||
await updateArtifactStore();
|
||||
|
||||
// 2. clear resource cache
|
||||
@@ -79,7 +79,7 @@ async function refresh(_, res) {
|
||||
async function gistBackup(req, res) {
|
||||
const { action } = req.query;
|
||||
// read token
|
||||
const { gistToken } = $.read(SETTINGS_KEY);
|
||||
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
|
||||
if (!gistToken) {
|
||||
failed(
|
||||
res,
|
||||
@@ -92,6 +92,7 @@ async function gistBackup(req, res) {
|
||||
const gist = new Gist({
|
||||
token: gistToken,
|
||||
key: GIST_BACKUP_KEY,
|
||||
syncPlatform,
|
||||
});
|
||||
try {
|
||||
let content;
|
||||
|
||||
54
backend/src/restful/parser.js
Normal file
54
backend/src/restful/parser.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import { success, failed } from '@/restful/response';
|
||||
import { ProxyUtils } from '@/core/proxy-utils';
|
||||
import { RuleUtils } from '@/core/rule-utils';
|
||||
|
||||
export default function register($app) {
|
||||
$app.route('/api/proxy/parse').post(proxy_parser);
|
||||
$app.route('/api/rule/parse').post(rule_parser);
|
||||
}
|
||||
|
||||
/***
|
||||
* 感谢 izhangxm 的 PR!
|
||||
* 目前没有节点操作, 没有支持完整参数, 以后再完善一下
|
||||
*/
|
||||
|
||||
/***
|
||||
* 代理服务器协议转换接口。
|
||||
* 请求方法为POST,数据为json。需要提供data和client字段。
|
||||
* data: string, 协议数据,每行一个或者是clash
|
||||
* client: string, 目标平台名称,见backend/src/core/proxy-utils/producers/index.js
|
||||
*
|
||||
*/
|
||||
function proxy_parser(req, res) {
|
||||
const { data, client, content, platform } = req.body;
|
||||
var result = {};
|
||||
try {
|
||||
var proxies = ProxyUtils.parse(data ?? content);
|
||||
var par_res = ProxyUtils.produce(proxies, client ?? platform);
|
||||
result['par_res'] = par_res;
|
||||
} catch (err) {
|
||||
failed(res, err);
|
||||
return;
|
||||
}
|
||||
success(res, result);
|
||||
}
|
||||
/**
|
||||
* 规则转换接口。
|
||||
* 请求方法为POST,数据为json。需要提供data和client字段。
|
||||
* data: string, 多行规则字符串
|
||||
* client: string, 目标平台名称,具体见backend/src/core/rule-utils/producers.js
|
||||
*/
|
||||
function rule_parser(req, res) {
|
||||
const { data, client, content, platform } = req.body;
|
||||
var result = {};
|
||||
try {
|
||||
const rules = RuleUtils.parse(data ?? content);
|
||||
var par_res = RuleUtils.produce(rules, client ?? platform);
|
||||
result['par_res'] = par_res;
|
||||
} catch (err) {
|
||||
failed(res, err);
|
||||
return;
|
||||
}
|
||||
|
||||
success(res, result);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SETTINGS_KEY, ARTIFACT_REPOSITORY_KEY } from '@/constants';
|
||||
import { success } from './response';
|
||||
import { success, failed } from './response';
|
||||
import { InternalServerError } from '@/restful/errors';
|
||||
import $ from '@/core/app';
|
||||
import Gist from '@/utils/gist';
|
||||
|
||||
@@ -10,51 +11,105 @@ export default function register($app) {
|
||||
}
|
||||
|
||||
async function getSettings(req, res) {
|
||||
let settings = $.read(SETTINGS_KEY);
|
||||
if (!settings) {
|
||||
settings = {};
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
}
|
||||
try {
|
||||
let settings = $.read(SETTINGS_KEY);
|
||||
if (!settings) {
|
||||
settings = {};
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
}
|
||||
|
||||
if (!settings.avatarUrl) await updateGitHubAvatar();
|
||||
if (!settings.artifactStore) await updateArtifactStore();
|
||||
success(res, settings);
|
||||
if (!settings.avatarUrl) await updateAvatar();
|
||||
if (!settings.artifactStore) await updateArtifactStore();
|
||||
|
||||
success(res, settings);
|
||||
} catch (e) {
|
||||
$.error(`Failed to get settings: ${e.message ?? e}`);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
`FAILED_TO_GET_SETTINGS`,
|
||||
`Failed to get settings`,
|
||||
`Reason: ${e.message ?? e}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSettings(req, res) {
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const newSettings = {
|
||||
...settings,
|
||||
...req.body,
|
||||
};
|
||||
$.write(newSettings, SETTINGS_KEY);
|
||||
await updateGitHubAvatar();
|
||||
await updateArtifactStore();
|
||||
success(res, newSettings);
|
||||
try {
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const newSettings = {
|
||||
...settings,
|
||||
...req.body,
|
||||
};
|
||||
$.write(newSettings, SETTINGS_KEY);
|
||||
await updateAvatar();
|
||||
await updateArtifactStore();
|
||||
success(res, newSettings);
|
||||
} catch (e) {
|
||||
$.error(`Failed to update settings: ${e.message ?? e}`);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
`FAILED_TO_UPDATE_SETTINGS`,
|
||||
`Failed to update settings`,
|
||||
`Reason: ${e.message ?? e}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateGitHubAvatar() {
|
||||
export async function updateAvatar() {
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const username = settings.githubUser;
|
||||
const { githubUser: username, syncPlatform } = settings;
|
||||
if (username) {
|
||||
try {
|
||||
const data = await $.http
|
||||
.get({
|
||||
url: `https://api.github.com/users/${username}`,
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
|
||||
},
|
||||
})
|
||||
.then((resp) => JSON.parse(resp.body));
|
||||
settings.avatarUrl = data['avatar_url'];
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Failed to fetch GitHub avatar for User: ${username}. Reason: ${
|
||||
err.message ?? err
|
||||
}`,
|
||||
);
|
||||
if (syncPlatform === 'gitlab') {
|
||||
try {
|
||||
const data = await $.http
|
||||
.get({
|
||||
url: `https://gitlab.com/api/v4/users?username=${encodeURIComponent(
|
||||
username,
|
||||
)}`,
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
|
||||
},
|
||||
})
|
||||
.then((resp) => JSON.parse(resp.body));
|
||||
settings.avatarUrl = data[0]['avatar_url'].replace(
|
||||
/(\?|&)s=\d+(&|$)/,
|
||||
'$1s=160$2',
|
||||
);
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Failed to fetch GitLab avatar for User: ${username}. Reason: ${
|
||||
err.message ?? err
|
||||
}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const data = await $.http
|
||||
.get({
|
||||
url: `https://api.github.com/users/${encodeURIComponent(
|
||||
username,
|
||||
)}`,
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
|
||||
},
|
||||
})
|
||||
.then((resp) => JSON.parse(resp.body));
|
||||
settings.avatarUrl = data['avatar_url'];
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Failed to fetch GitHub avatar for User: ${username}. Reason: ${
|
||||
err.message ?? err
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,25 +117,30 @@ export async function updateGitHubAvatar() {
|
||||
export async function updateArtifactStore() {
|
||||
$.log('Updating artifact store');
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const { githubUser, gistToken } = settings;
|
||||
if (githubUser && gistToken) {
|
||||
const { gistToken, syncPlatform } = settings;
|
||||
if (gistToken) {
|
||||
const manager = new Gist({
|
||||
token: gistToken,
|
||||
key: ARTIFACT_REPOSITORY_KEY,
|
||||
syncPlatform,
|
||||
});
|
||||
|
||||
try {
|
||||
const gistId = await manager.locate();
|
||||
if (gistId !== -1) {
|
||||
settings.artifactStore = `https://gist.github.com/${githubUser}/${gistId}`;
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
const gist = await manager.locate();
|
||||
const url = gist?.html_url ?? gist?.web_url;
|
||||
if (url) {
|
||||
$.log(`找到 Sub-Store Gist: ${url}`);
|
||||
// 只需要保证 token 是对的, 现在 username 错误只会导致头像错误
|
||||
settings.artifactStore = url;
|
||||
settings.artifactStoreStatus = 'VALID';
|
||||
} else {
|
||||
$.error(`找不到 Sub-Store Gist`);
|
||||
settings.artifactStoreStatus = 'NOT FOUND';
|
||||
}
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Failed to fetch artifact store for User: ${githubUser}. Reason: ${
|
||||
err.message ?? err
|
||||
}`,
|
||||
);
|
||||
$.error(`查找 Sub-Store Gist 时发生错误: ${err.message ?? err}`);
|
||||
settings.artifactStoreStatus = 'ERROR';
|
||||
}
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,23 +447,84 @@ async function syncArtifacts() {
|
||||
const files = {};
|
||||
|
||||
try {
|
||||
const invalid = [];
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
const subNames = [];
|
||||
allArtifacts.map((artifact) => {
|
||||
if (artifact.sync && artifact.source) {
|
||||
if (artifact.type === 'subscription') {
|
||||
const subName = artifact.source;
|
||||
const sub = findByName(allSubs, subName);
|
||||
if (sub && sub.url && !subNames.includes(subName)) {
|
||||
subNames.push(subName);
|
||||
}
|
||||
} else if (artifact.type === 'collection') {
|
||||
const collection = findByName(allCols, artifact.source);
|
||||
if (collection && Array.isArray(collection.subscriptions)) {
|
||||
collection.subscriptions.map((subName) => {
|
||||
const sub = findByName(allSubs, subName);
|
||||
if (sub && sub.url && !subNames.includes(subName)) {
|
||||
subNames.push(subName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (subNames.length > 0) {
|
||||
await Promise.all(
|
||||
subNames.map(async (subName) => {
|
||||
try {
|
||||
await produceArtifact({
|
||||
type: 'subscription',
|
||||
name: subName,
|
||||
});
|
||||
} catch (e) {
|
||||
// $.error(`${e.message ?? e}`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
allArtifacts.map(async (artifact) => {
|
||||
if (artifact.sync) {
|
||||
$.info(`正在同步云配置:${artifact.name}...`);
|
||||
const output = await produceArtifact({
|
||||
type: artifact.type,
|
||||
name: artifact.source,
|
||||
platform: artifact.platform,
|
||||
});
|
||||
try {
|
||||
if (artifact.sync && artifact.source) {
|
||||
$.info(`正在同步云配置:${artifact.name}...`);
|
||||
const output = await produceArtifact({
|
||||
type: artifact.type,
|
||||
name: artifact.source,
|
||||
platform: artifact.platform,
|
||||
produceOpts: {
|
||||
'include-unsupported-proxy':
|
||||
artifact.includeUnsupportedProxy,
|
||||
},
|
||||
});
|
||||
|
||||
files[artifact.name] = {
|
||||
content: output,
|
||||
};
|
||||
// if (!output || output.length === 0)
|
||||
// throw new Error('该配置的结果为空 不进行上传');
|
||||
|
||||
files[encodeURIComponent(artifact.name)] = {
|
||||
content: output,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`同步配置 ${artifact.name} 发生错误: ${e.message ?? e}`,
|
||||
);
|
||||
invalid.push(artifact.name);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (invalid.length > 0) {
|
||||
throw new Error(
|
||||
`同步配置 ${invalid.join(', ')} 发生错误 详情请查看日志`,
|
||||
);
|
||||
}
|
||||
|
||||
const resp = await syncToGist(files);
|
||||
const body = JSON.parse(resp.body);
|
||||
|
||||
@@ -471,10 +532,18 @@ async function syncArtifacts() {
|
||||
if (artifact.sync) {
|
||||
artifact.updated = new Date().getTime();
|
||||
// extract real url from gist
|
||||
artifact.url = body.files[artifact.name].raw_url.replace(
|
||||
/\/raw\/[^/]*\/(.*)/,
|
||||
'/raw/$1',
|
||||
);
|
||||
let files = body.files;
|
||||
let isGitLab;
|
||||
if (Array.isArray(files)) {
|
||||
isGitLab = true;
|
||||
files = Object.fromEntries(
|
||||
files.map((item) => [item.path, item]),
|
||||
);
|
||||
}
|
||||
const url = files[encodeURIComponent(artifact.name)]?.raw_url;
|
||||
artifact.url = isGitLab
|
||||
? url
|
||||
: url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,16 +559,16 @@ async function syncAllArtifacts(_, res) {
|
||||
try {
|
||||
await syncArtifacts();
|
||||
success(res);
|
||||
} catch (err) {
|
||||
} catch (e) {
|
||||
$.error(`同步订阅失败,原因:${e.message ?? e}`);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
`FAILED_TO_SYNC_ARTIFACTS`,
|
||||
`Failed to sync all artifacts`,
|
||||
`Reason: ${err}`,
|
||||
`Reason: ${e.message ?? e}`,
|
||||
),
|
||||
);
|
||||
$.info(`同步订阅失败,原因:${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,7 +585,20 @@ async function syncArtifact(req, res) {
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`Artifact ${name} does not exist!`,
|
||||
`找不到远程配置 ${name}`,
|
||||
),
|
||||
404,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!artifact.source) {
|
||||
$.error(`远程配置 ${name} 未设置来源`);
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
'RESOURCE_HAS_NO_SOURCE',
|
||||
`远程配置 ${name} 未设置来源`,
|
||||
),
|
||||
404,
|
||||
);
|
||||
@@ -528,6 +610,9 @@ async function syncArtifact(req, res) {
|
||||
type: artifact.type,
|
||||
name: artifact.source,
|
||||
platform: artifact.platform,
|
||||
produceOpts: {
|
||||
'include-unsupported-proxy': artifact.includeUnsupportedProxy,
|
||||
},
|
||||
});
|
||||
|
||||
$.info(
|
||||
@@ -537,6 +622,8 @@ async function syncArtifact(req, res) {
|
||||
2,
|
||||
)}`,
|
||||
);
|
||||
// if (!output || output.length === 0)
|
||||
// throw new Error('该配置的结果为空 不进行上传');
|
||||
const resp = await syncToGist({
|
||||
[encodeURIComponent(artifact.name)]: {
|
||||
content: output,
|
||||
@@ -544,13 +631,20 @@ async function syncArtifact(req, res) {
|
||||
});
|
||||
artifact.updated = new Date().getTime();
|
||||
const body = JSON.parse(resp.body);
|
||||
artifact.url = body.files[
|
||||
encodeURIComponent(artifact.name)
|
||||
].raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
|
||||
let files = body.files;
|
||||
let isGitLab;
|
||||
if (Array.isArray(files)) {
|
||||
isGitLab = true;
|
||||
files = Object.fromEntries(files.map((item) => [item.path, item]));
|
||||
}
|
||||
const url = files[encodeURIComponent(artifact.name)]?.raw_url;
|
||||
artifact.url = isGitLab
|
||||
? url
|
||||
: url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
success(res, artifact);
|
||||
} catch (err) {
|
||||
$.error(`远程配置 ${artifact.name} 发生错误: ${err}`);
|
||||
$.error(`远程配置 ${artifact.name} 发生错误: ${err.message ?? err}`);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import { FILES_KEY, MODULES_KEY, SETTINGS_KEY } from '@/constants';
|
||||
import { findByName } from '@/utils/database';
|
||||
import { SETTINGS_KEY } from '@/constants';
|
||||
import { HTTP, ENV } from '@/vendor/open-api';
|
||||
import { hex_md5 } from '@/vendor/md5';
|
||||
import resourceCache from '@/utils/resource-cache';
|
||||
import headersResourceCache from '@/utils/headers-resource-cache';
|
||||
import { getFlowField } from '@/utils/flow';
|
||||
import {
|
||||
getFlowField,
|
||||
getFlowHeaders,
|
||||
parseFlowHeaders,
|
||||
validCheck,
|
||||
} from '@/utils/flow';
|
||||
import $ from '@/core/app';
|
||||
|
||||
const tasks = new Map();
|
||||
|
||||
export default async function download(url, ua, timeout) {
|
||||
export default async function download(rawUrl, ua, timeout) {
|
||||
let $arguments = {};
|
||||
let url = rawUrl.replace(/#noFlow$/, '');
|
||||
const rawArgs = url.split('#');
|
||||
url = url.split('#')[0];
|
||||
if (rawArgs.length > 1) {
|
||||
try {
|
||||
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
|
||||
@@ -29,25 +35,26 @@ export default async function download(url, ua, timeout) {
|
||||
}
|
||||
}
|
||||
|
||||
const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/);
|
||||
if (downloadUrlMatch) {
|
||||
let type = downloadUrlMatch?.[1];
|
||||
let name = downloadUrlMatch?.[2];
|
||||
if (name == null) {
|
||||
throw new Error(`本地 ${type} URL 无效: ${url}`);
|
||||
}
|
||||
name = decodeURIComponent(name);
|
||||
const key = type === 'module' ? MODULES_KEY : FILES_KEY;
|
||||
const item = findByName($.read(key), name);
|
||||
if (!item) {
|
||||
throw new Error(`找不到本地 ${type}: ${name}`);
|
||||
}
|
||||
// const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/);
|
||||
// if (downloadUrlMatch) {
|
||||
// let type = downloadUrlMatch?.[1];
|
||||
// let name = downloadUrlMatch?.[2];
|
||||
// if (name == null) {
|
||||
// throw new Error(`本地 ${type} URL 无效: ${url}`);
|
||||
// }
|
||||
// name = decodeURIComponent(name);
|
||||
// const key = type === 'module' ? MODULES_KEY : FILES_KEY;
|
||||
// const item = findByName($.read(key), name);
|
||||
// if (!item) {
|
||||
// throw new Error(`找不到本地 ${type}: ${name}`);
|
||||
// }
|
||||
|
||||
return item.content;
|
||||
}
|
||||
// return item.content;
|
||||
// }
|
||||
|
||||
const { isNode } = ENV();
|
||||
const { defaultUserAgent, defaultTimeout } = $.read(SETTINGS_KEY);
|
||||
const { defaultUserAgent, defaultTimeout, cacheThreshold } =
|
||||
$.read(SETTINGS_KEY);
|
||||
const userAgent = ua || defaultUserAgent || 'clash.meta';
|
||||
const requestTimeout = timeout || defaultTimeout;
|
||||
const id = hex_md5(userAgent + url);
|
||||
@@ -62,36 +69,55 @@ export default async function download(url, ua, timeout) {
|
||||
timeout: requestTimeout,
|
||||
});
|
||||
|
||||
const result = new Promise((resolve, reject) => {
|
||||
// try to find in app cache
|
||||
const cached = resourceCache.get(id);
|
||||
if (!$arguments?.noCache && cached) {
|
||||
resolve(cached);
|
||||
} else {
|
||||
$.info(
|
||||
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nURL: ${url}`,
|
||||
);
|
||||
http.get(url)
|
||||
.then((resp) => {
|
||||
const { body, headers } = resp;
|
||||
if (headers) {
|
||||
const flowInfo = getFlowField(headers);
|
||||
if (flowInfo) {
|
||||
headersResourceCache.set(url, flowInfo);
|
||||
}
|
||||
}
|
||||
if (body.replace(/\s/g, '').length === 0)
|
||||
reject(new Error('远程资源内容为空!'));
|
||||
else {
|
||||
resourceCache.set(id, body);
|
||||
resolve(body);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error(`无法下载 URL:${url}`));
|
||||
});
|
||||
let result;
|
||||
|
||||
// try to find in app cache
|
||||
const cached = resourceCache.get(id);
|
||||
if (!$arguments?.noCache && cached) {
|
||||
$.info(`使用缓存: ${url}`);
|
||||
result = cached;
|
||||
} else {
|
||||
$.info(
|
||||
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nURL: ${url}`,
|
||||
);
|
||||
try {
|
||||
const { body, headers } = await http.get(url);
|
||||
|
||||
if (headers) {
|
||||
const flowInfo = getFlowField(headers);
|
||||
if (flowInfo) {
|
||||
headersResourceCache.set(url, flowInfo);
|
||||
}
|
||||
}
|
||||
if (body.replace(/\s/g, '').length === 0)
|
||||
throw new Error(new Error('远程资源内容为空'));
|
||||
let shouldCache = true;
|
||||
if (cacheThreshold) {
|
||||
const size = body.length / 1024;
|
||||
if (size > cacheThreshold) {
|
||||
$.info(
|
||||
`资源大小 ${size.toFixed(
|
||||
2,
|
||||
)} KB 超过了 ${cacheThreshold} KB, 不缓存`,
|
||||
);
|
||||
shouldCache = false;
|
||||
}
|
||||
}
|
||||
if (shouldCache) {
|
||||
resourceCache.set(id, body);
|
||||
}
|
||||
|
||||
result = body;
|
||||
} catch (e) {
|
||||
throw new Error(`无法下载 URL ${url}: ${e.message ?? e}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 检查订阅有效性
|
||||
|
||||
if ($arguments?.validCheck) {
|
||||
await validCheck(parseFlowHeaders(await getFlowHeaders(url)));
|
||||
}
|
||||
|
||||
if (!isNode) {
|
||||
tasks.set(id, result);
|
||||
|
||||
@@ -9,9 +9,11 @@ export function getFlowField(headers) {
|
||||
)[0];
|
||||
return headers[subkey];
|
||||
}
|
||||
export async function getFlowHeaders(url, ua, timeout) {
|
||||
export async function getFlowHeaders(rawUrl, ua, timeout) {
|
||||
let url = rawUrl;
|
||||
let $arguments = {};
|
||||
const rawArgs = url.split('#');
|
||||
url = url.split('#')[0];
|
||||
if (rawArgs.length > 1) {
|
||||
try {
|
||||
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
|
||||
@@ -28,6 +30,9 @@ export async function getFlowHeaders(url, ua, timeout) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($arguments?.noFlow) {
|
||||
return;
|
||||
}
|
||||
const cached = headersResourceCache.get(url);
|
||||
let flowInfo;
|
||||
if (!$arguments?.noCache && cached) {
|
||||
@@ -83,17 +88,27 @@ export async function getFlowHeaders(url, ua, timeout) {
|
||||
export function parseFlowHeaders(flowHeaders) {
|
||||
if (!flowHeaders) return;
|
||||
// unit is KB
|
||||
const uploadMatch = flowHeaders.match(/upload=(-?)(\d+)/);
|
||||
const uploadMatch = flowHeaders.match(
|
||||
/upload=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
|
||||
);
|
||||
const upload = Number(uploadMatch[1] + uploadMatch[2]);
|
||||
|
||||
const downloadMatch = flowHeaders.match(/download=(-?)(\d+)/);
|
||||
const downloadMatch = flowHeaders.match(
|
||||
/download=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
|
||||
);
|
||||
const download = Number(downloadMatch[1] + downloadMatch[2]);
|
||||
|
||||
const total = Number(flowHeaders.match(/total=(\d+)/)[1]);
|
||||
const totalMatch = flowHeaders.match(
|
||||
/total=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
|
||||
);
|
||||
const total = Number(totalMatch[1] + totalMatch[2]);
|
||||
|
||||
// optional expire timestamp
|
||||
const match = flowHeaders.match(/expire=(\d+)/);
|
||||
const expires = match ? Number(match[1]) : undefined;
|
||||
const expireMatch = flowHeaders.match(
|
||||
/expire=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
|
||||
);
|
||||
const expires = expireMatch
|
||||
? Number(expireMatch[1] + expireMatch[2])
|
||||
: undefined;
|
||||
|
||||
return { expires, total, usage: { upload, download } };
|
||||
}
|
||||
@@ -105,3 +120,26 @@ export function flowTransfer(flow, unit = 'B') {
|
||||
? { value: flow.toFixed(1), unit: unit }
|
||||
: flowTransfer(flow / 1024, unitList[++unitIndex]);
|
||||
}
|
||||
|
||||
export function validCheck(flow) {
|
||||
if (!flow) {
|
||||
throw new Error('没有流量信息');
|
||||
}
|
||||
if (flow?.expires && flow.expires * 1000 < Date.now()) {
|
||||
const date = new Date(flow.expires * 1000).toLocaleDateString();
|
||||
throw new Error(`订阅已过期: ${date}`);
|
||||
}
|
||||
if (flow?.total) {
|
||||
const upload = flow.usage?.upload || 0;
|
||||
const download = flow.usage?.download || 0;
|
||||
if (flow.total - upload - download < 0) {
|
||||
const current = upload + download;
|
||||
const currT = flowTransfer(Math.abs(current));
|
||||
currT.value = current < 0 ? '-' + currT.value : currT.value;
|
||||
const totalT = flowTransfer(flow.total);
|
||||
throw new Error(
|
||||
`流量已用完: ${currT.value} ${currT.unit} / ${totalT.value} ${totalT.unit}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,11 +213,16 @@ export function getFlag(name) {
|
||||
'🇹🇼': [
|
||||
'Taiwan',
|
||||
'台湾',
|
||||
'臺灣',
|
||||
'台灣',
|
||||
'中華民國',
|
||||
'中华民国',
|
||||
'台北',
|
||||
'台中',
|
||||
'新北',
|
||||
'彰化',
|
||||
'台',
|
||||
'臺',
|
||||
'Taipei',
|
||||
],
|
||||
'🇺🇦': ['Ukraine', '乌克兰', '烏克蘭'],
|
||||
@@ -371,7 +376,7 @@ export function getFlag(name) {
|
||||
'🇹🇭': ['TH', 'THA'],
|
||||
'🇹🇳': ['TN', 'TUN'],
|
||||
'🇹🇷': ['TR', 'TUR'],
|
||||
'🇹🇼': ['TW', 'TWN', 'CHT', 'HINET'],
|
||||
'🇹🇼': ['TW', 'TWN', 'CHT', 'HINET', 'ROC'],
|
||||
'🇺🇦': ['UA', 'UKR'],
|
||||
'🇺🇸': ['US', 'USA', 'LAX', 'SFO'],
|
||||
'🇺🇾': ['UY', 'URY'],
|
||||
|
||||
@@ -4,81 +4,233 @@ import { HTTP } from '@/vendor/open-api';
|
||||
* Gist backup
|
||||
*/
|
||||
export default class Gist {
|
||||
constructor({ token, key }) {
|
||||
this.http = HTTP({
|
||||
baseURL: 'https://api.github.com',
|
||||
headers: {
|
||||
constructor({ token, key, syncPlatform }) {
|
||||
if (syncPlatform === 'gitlab') {
|
||||
this.headers = {
|
||||
'PRIVATE-TOKEN': `${token}`,
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
|
||||
};
|
||||
this.http = HTTP({
|
||||
baseURL: 'https://gitlab.com/api/v4',
|
||||
headers: { ...this.headers },
|
||||
events: {
|
||||
onResponse: (resp) => {
|
||||
if (/^[45]/.test(String(resp.statusCode))) {
|
||||
const body = JSON.parse(resp.body);
|
||||
return Promise.reject(
|
||||
`ERROR: ${body.message?.error ?? body.message}`,
|
||||
);
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.headers = {
|
||||
Authorization: `token ${token}`,
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
|
||||
},
|
||||
events: {
|
||||
onResponse: (resp) => {
|
||||
if (/^[45]/.test(String(resp.statusCode))) {
|
||||
return Promise.reject(
|
||||
`ERROR: ${JSON.parse(resp.body).message}`,
|
||||
);
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
};
|
||||
this.http = HTTP({
|
||||
baseURL: 'https://api.github.com',
|
||||
headers: { ...this.headers },
|
||||
events: {
|
||||
onResponse: (resp) => {
|
||||
if (/^[45]/.test(String(resp.statusCode))) {
|
||||
return Promise.reject(
|
||||
`ERROR: ${JSON.parse(resp.body).message}`,
|
||||
);
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.key = key;
|
||||
this.syncPlatform = syncPlatform;
|
||||
}
|
||||
|
||||
async locate() {
|
||||
return this.http.get('/gists').then((response) => {
|
||||
const gists = JSON.parse(response.body);
|
||||
for (let g of gists) {
|
||||
if (g.description === this.key) {
|
||||
return g.id;
|
||||
if (this.syncPlatform === 'gitlab') {
|
||||
return this.http.get('/snippets').then((response) => {
|
||||
const gists = JSON.parse(response.body);
|
||||
|
||||
for (let g of gists) {
|
||||
if (g.title === this.key) {
|
||||
return g;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
return;
|
||||
});
|
||||
} else {
|
||||
return this.http.get('/gists').then((response) => {
|
||||
const gists = JSON.parse(response.body);
|
||||
for (let g of gists) {
|
||||
if (g.description === this.key) {
|
||||
return g;
|
||||
}
|
||||
}
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async upload(files) {
|
||||
if (Object.keys(files).length === 0) {
|
||||
async upload(input) {
|
||||
if (Object.keys(input).length === 0) {
|
||||
return Promise.reject('未提供需上传的文件');
|
||||
}
|
||||
|
||||
const id = await this.locate();
|
||||
const gist = await this.locate();
|
||||
|
||||
if (id === -1) {
|
||||
// create a new gist for backup
|
||||
return this.http.post({
|
||||
url: '/gists',
|
||||
body: JSON.stringify({
|
||||
description: this.key,
|
||||
public: false,
|
||||
files,
|
||||
}),
|
||||
let files = input;
|
||||
|
||||
if (gist?.id) {
|
||||
if (this.syncPlatform === 'gitlab') {
|
||||
gist.files = gist.files.reduce((acc, item) => {
|
||||
acc[item.path] = item;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
// console.log(`files`, files);
|
||||
// console.log(`gist`, gist.files);
|
||||
let actions = [];
|
||||
const result = { ...gist.files };
|
||||
Object.keys(files).map((key) => {
|
||||
if (result[key]) {
|
||||
if (
|
||||
files[key].content == null ||
|
||||
files[key].content === ''
|
||||
) {
|
||||
delete result[key];
|
||||
actions.push({
|
||||
action: 'delete',
|
||||
file_path: key,
|
||||
});
|
||||
} else {
|
||||
result[key] = files[key];
|
||||
actions.push({
|
||||
action: 'update',
|
||||
file_path: key,
|
||||
content: files[key].content,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
files[key].content == null ||
|
||||
files[key].content === ''
|
||||
) {
|
||||
delete result[key];
|
||||
delete files[key];
|
||||
} else {
|
||||
result[key] = files[key];
|
||||
actions.push({
|
||||
action: 'create',
|
||||
file_path: key,
|
||||
content: files[key].content,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log(`result`, result);
|
||||
console.log(`files`, files);
|
||||
console.log(`actions`, actions);
|
||||
|
||||
if (this.syncPlatform === 'gitlab') {
|
||||
if (Object.keys(result).length === 0) {
|
||||
return Promise.reject(
|
||||
'本次操作将导致所有文件的内容都为空, 无法更新 snippet',
|
||||
);
|
||||
}
|
||||
if (Object.keys(result).length > 10) {
|
||||
return Promise.reject(
|
||||
'本次操作将导致 snippet 的文件数超过 10, 无法更新 snippet',
|
||||
);
|
||||
}
|
||||
files = actions;
|
||||
return this.http.put({
|
||||
headers: {
|
||||
...this.headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
url: `/snippets/${gist.id}`,
|
||||
body: JSON.stringify({ files }),
|
||||
});
|
||||
} else {
|
||||
if (Object.keys(result).length === 0) {
|
||||
return Promise.reject(
|
||||
'本次操作将导致所有文件的内容都为空, 无法更新 gist',
|
||||
);
|
||||
}
|
||||
return this.http.patch({
|
||||
url: `/gists/${gist.id}`,
|
||||
body: JSON.stringify({ files }),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// update an existing gist
|
||||
return this.http.patch({
|
||||
url: `/gists/${id}`,
|
||||
body: JSON.stringify({ files }),
|
||||
});
|
||||
files = Object.entries(files).reduce((acc, [key, file]) => {
|
||||
if (file.content !== null && file.content !== '') {
|
||||
acc[key] = file;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
if (this.syncPlatform === 'gitlab') {
|
||||
if (Object.keys(files).length === 0) {
|
||||
return Promise.reject(
|
||||
'所有文件的内容都为空, 无法创建 snippet',
|
||||
);
|
||||
}
|
||||
files = Object.keys(files).map((key) => ({
|
||||
file_path: key,
|
||||
content: files[key].content,
|
||||
}));
|
||||
return this.http.post({
|
||||
headers: {
|
||||
...this.headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
url: '/snippets',
|
||||
body: JSON.stringify({
|
||||
title: this.key,
|
||||
visibility: 'private',
|
||||
files,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
if (Object.keys(files).length === 0) {
|
||||
return Promise.reject(
|
||||
'所有文件的内容都为空, 无法创建 gist',
|
||||
);
|
||||
}
|
||||
return this.http.post({
|
||||
url: '/gists',
|
||||
body: JSON.stringify({
|
||||
description: this.key,
|
||||
public: false,
|
||||
files,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async download(filename) {
|
||||
const id = await this.locate();
|
||||
if (id === -1) {
|
||||
return Promise.reject('未找到Gist备份!');
|
||||
} else {
|
||||
const gist = await this.locate();
|
||||
if (gist?.id) {
|
||||
try {
|
||||
const { files } = await this.http
|
||||
.get(`/gists/${id}`)
|
||||
.get(`/gists/${gist.id}`)
|
||||
.then((resp) => JSON.parse(resp.body));
|
||||
const url = files[filename].raw_url;
|
||||
return await this.http.get(url).then((resp) => resp.body);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject('找不到 Sub-Store Gist');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,17 @@ class ResourceCache {
|
||||
if (!$.read(HEADERS_RESOURCE_CACHE_KEY)) {
|
||||
$.write('{}', HEADERS_RESOURCE_CACHE_KEY);
|
||||
}
|
||||
this.resourceCache = JSON.parse($.read(HEADERS_RESOURCE_CACHE_KEY));
|
||||
try {
|
||||
this.resourceCache = JSON.parse($.read(HEADERS_RESOURCE_CACHE_KEY));
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`解析持久化缓存中的 ${HEADERS_RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${
|
||||
e?.message ?? e
|
||||
}`,
|
||||
);
|
||||
this.resourceCache = {};
|
||||
$.write('{}', HEADERS_RESOURCE_CACHE_KEY);
|
||||
}
|
||||
this._cleanup();
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,53 @@ function getIfPresent(obj, defaultValue) {
|
||||
return isPresent(obj) ? obj : defaultValue;
|
||||
}
|
||||
|
||||
const utf8ArrayToStr =
|
||||
typeof TextDecoder !== 'undefined'
|
||||
? (v) => new TextDecoder().decode(new Uint8Array(v))
|
||||
: (function () {
|
||||
var charCache = new Array(128); // Preallocate the cache for the common single byte chars
|
||||
var charFromCodePt = String.fromCodePoint || String.fromCharCode;
|
||||
var result = [];
|
||||
|
||||
return function (array) {
|
||||
var codePt, byte1;
|
||||
var buffLen = array.length;
|
||||
|
||||
result.length = 0;
|
||||
|
||||
for (var i = 0; i < buffLen; ) {
|
||||
byte1 = array[i++];
|
||||
|
||||
if (byte1 <= 0x7f) {
|
||||
codePt = byte1;
|
||||
} else if (byte1 <= 0xdf) {
|
||||
codePt = ((byte1 & 0x1f) << 6) | (array[i++] & 0x3f);
|
||||
} else if (byte1 <= 0xef) {
|
||||
codePt =
|
||||
((byte1 & 0x0f) << 12) |
|
||||
((array[i++] & 0x3f) << 6) |
|
||||
(array[i++] & 0x3f);
|
||||
} else if (String.fromCodePoint) {
|
||||
codePt =
|
||||
((byte1 & 0x07) << 18) |
|
||||
((array[i++] & 0x3f) << 12) |
|
||||
((array[i++] & 0x3f) << 6) |
|
||||
(array[i++] & 0x3f);
|
||||
} else {
|
||||
codePt = 63; // Cannot convert four byte code points, so use "?" instead
|
||||
i += 3;
|
||||
}
|
||||
|
||||
result.push(
|
||||
charCache[codePt] ||
|
||||
(charCache[codePt] = charFromCodePt(codePt)),
|
||||
);
|
||||
}
|
||||
|
||||
return result.join('');
|
||||
};
|
||||
})();
|
||||
|
||||
export {
|
||||
isIPv4,
|
||||
isIPv6,
|
||||
@@ -43,4 +90,5 @@ export {
|
||||
getIfNotBlank,
|
||||
isPresent,
|
||||
getIfPresent,
|
||||
utf8ArrayToStr,
|
||||
};
|
||||
|
||||
@@ -7,7 +7,17 @@ class ResourceCache {
|
||||
if (!$.read(RESOURCE_CACHE_KEY)) {
|
||||
$.write('{}', RESOURCE_CACHE_KEY);
|
||||
}
|
||||
this.resourceCache = JSON.parse($.read(RESOURCE_CACHE_KEY));
|
||||
try {
|
||||
this.resourceCache = JSON.parse($.read(RESOURCE_CACHE_KEY));
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`解析持久化缓存中的 ${RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${
|
||||
e?.message ?? e
|
||||
}`,
|
||||
);
|
||||
this.resourceCache = {};
|
||||
$.write('{}', RESOURCE_CACHE_KEY);
|
||||
}
|
||||
this._cleanup();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,17 @@ class ResourceCache {
|
||||
if (!$.read(SCRIPT_RESOURCE_CACHE_KEY)) {
|
||||
$.write('{}', SCRIPT_RESOURCE_CACHE_KEY);
|
||||
}
|
||||
this.resourceCache = JSON.parse($.read(SCRIPT_RESOURCE_CACHE_KEY));
|
||||
try {
|
||||
this.resourceCache = JSON.parse($.read(SCRIPT_RESOURCE_CACHE_KEY));
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`解析持久化缓存中的 ${SCRIPT_RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${
|
||||
e?.message ?? e
|
||||
}`,
|
||||
);
|
||||
this.resourceCache = {};
|
||||
$.write('{}', SCRIPT_RESOURCE_CACHE_KEY);
|
||||
}
|
||||
this._cleanup();
|
||||
}
|
||||
|
||||
|
||||
37
backend/src/utils/yaml.js
Normal file
37
backend/src/utils/yaml.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import YAML from 'static-js-yaml';
|
||||
|
||||
function retry(fn, content, ...args) {
|
||||
try {
|
||||
return fn(content, ...args);
|
||||
} catch (e) {
|
||||
return fn(
|
||||
dump(
|
||||
fn(
|
||||
content.replace(/!<str>\s*/g, '__SubStoreJSYAMLString__'),
|
||||
...args,
|
||||
),
|
||||
).replace(/__SubStoreJSYAMLString__/g, ''),
|
||||
...args,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function safeLoad(content, ...args) {
|
||||
return retry(YAML.safeLoad, content, ...args);
|
||||
}
|
||||
export function load(content, ...args) {
|
||||
return retry(YAML.load, content, ...args);
|
||||
}
|
||||
export function safeDump(...args) {
|
||||
return YAML.safeDump(...args);
|
||||
}
|
||||
export function dump(...args) {
|
||||
return YAML.dump(...args);
|
||||
}
|
||||
|
||||
export default {
|
||||
safeLoad,
|
||||
load,
|
||||
safeDump,
|
||||
dump,
|
||||
};
|
||||
13
backend/src/vendor/open-api.js
vendored
13
backend/src/vendor/open-api.js
vendored
@@ -312,8 +312,19 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
|
||||
? eval("require('request')")
|
||||
: $httpClient;
|
||||
request[method.toLowerCase()](
|
||||
options,
|
||||
JSON.parse(JSON.stringify(options)),
|
||||
(err, response, body) => {
|
||||
// if (err) {
|
||||
// console.log(err);
|
||||
// } else {
|
||||
// console.log({
|
||||
// statusCode:
|
||||
// response.status || response.statusCode,
|
||||
// headers: response.headers,
|
||||
// body,
|
||||
// });
|
||||
// }
|
||||
|
||||
if (err) reject(err);
|
||||
else
|
||||
resolve({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!name=Sub-Store
|
||||
#!desc=高级订阅管理工具
|
||||
#!desc=高级订阅管理工具. 定时任务默认为每天 23 点 55 分
|
||||
#!openUrl=https://sub.store
|
||||
#!author=Peng-YM
|
||||
#!homepage=https://github.com/sub-store-org/Sub-Store
|
||||
@@ -17,4 +17,4 @@ hostname=sub.store
|
||||
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
|
||||
|
||||
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
|
||||
cron "55 23 * * *" script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name":"Sub-Store",
|
||||
"description":"",
|
||||
"task":[
|
||||
"0 0 * * * https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync, img-url=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png"
|
||||
]
|
||||
}
|
||||
"name": "Sub-Store",
|
||||
"description": "定时任务默认为每天 23 点 55 分",
|
||||
"task": [
|
||||
"55 23 * * * https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync, img-url=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png"
|
||||
]
|
||||
}
|
||||
@@ -12,9 +12,14 @@ Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
|
||||
安装使用 插件 [`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
|
||||
|
||||
0. 最新 Surge iOS TestFlight 版本 可使用 Beta 版(支持最新 Surge iOS TestFlight 版本的分类和参数设置): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.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)
|
||||
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)
|
||||
|
||||
> 最新 Surge iOS TestFlight 版本应该没有内存问题了 可以大胆尝试带 ability 参数版本
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
name: Sub-Store
|
||||
desc: 高级订阅管理工具 @Peng-YM
|
||||
desc: 高级订阅管理工具 @Peng-YM. 定时任务默认为每天 23 点 55 分
|
||||
icon: https://raw.githubusercontent.com/cc63/ICON/main/Sub-Store.png
|
||||
|
||||
http:
|
||||
mitm:
|
||||
@@ -19,7 +20,7 @@ http:
|
||||
cron:
|
||||
script:
|
||||
- name: cron-sync-artifacts
|
||||
cron: "0 0 * * *"
|
||||
cron: "55 23 * * *"
|
||||
timeout: 120
|
||||
|
||||
script-providers:
|
||||
@@ -33,4 +34,4 @@ script-providers:
|
||||
|
||||
cron-sync-artifacts:
|
||||
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
|
||||
interval: 86400
|
||||
interval: 86400
|
||||
|
||||
15
config/Surge-Beta.sgmodule
Normal file
15
config/Surge-Beta.sgmodule
Normal file
@@ -0,0 +1,15 @@
|
||||
#!name=Sub-Store(β)
|
||||
#!desc=支持最新 Surge iOS TestFlight 版本的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *
|
||||
#!category=订阅管理
|
||||
#!arguments=ability:http-client-policy,cronexp:55 23 * * *,sync:"Sub-Store Sync"
|
||||
#!arguments-desc="\n1️⃣ ability\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n⚠️ Surge 上时候可能会爆内存\n不需要使用的时候应该关闭\n填写任意其他值关闭\n\n2️⃣ cronexp\n同步配置定时任务\n默认为每天 23 点 55 分\n\n3️⃣ sync\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务"
|
||||
|
||||
[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="{{{ability}}}"
|
||||
|
||||
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
|
||||
|
||||
{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
|
||||
@@ -1,12 +1,13 @@
|
||||
#!name=Sub-Store
|
||||
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数
|
||||
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 23 点 55 分
|
||||
#!category=订阅管理
|
||||
|
||||
[MITM]
|
||||
hostname = %APPEND% sub.store
|
||||
|
||||
[Script]
|
||||
# 主程序 已经去掉 Sub-Store Core 的参数 [,ability=http-client-policy] 不会爆内存,这个参数在 Surge 非常占用内存; 如果不需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 则可以使用此脚本
|
||||
# 主程序 已经去掉 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
|
||||
Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!name=Sub-Store
|
||||
#!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 可能会爆内存, 如果不需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用不带 ability 参数版本
|
||||
#!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 可能会爆内存, 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用不带 ability 参数版本. 定时任务默认为每天 23 点 55 分
|
||||
#!category=订阅管理
|
||||
|
||||
[MITM]
|
||||
hostname = %APPEND% sub.store
|
||||
@@ -8,4 +9,4 @@ hostname = %APPEND% sub.store
|
||||
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
|
||||
Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!name=Sub-Store
|
||||
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数
|
||||
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 23 点 55 分
|
||||
#!category=订阅管理
|
||||
|
||||
[MITM]
|
||||
hostname = %APPEND% sub.store
|
||||
@@ -8,4 +9,4 @@ hostname = %APPEND% sub.store
|
||||
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
|
||||
Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
|
||||
|
||||
174
scripts/demo.js
Normal file
174
scripts/demo.js
Normal file
@@ -0,0 +1,174 @@
|
||||
function operator(proxies = [], targetPlatform, context) {
|
||||
// 支持快捷操作 不一定要写一个 function
|
||||
// 可参考 https://t.me/zhetengsha/970
|
||||
// https://t.me/zhetengsha/1009
|
||||
|
||||
|
||||
// proxies 为传入的内部节点数组
|
||||
// 结构大致参考了 Clash.Meta(mihomo) 有私货
|
||||
// 可在预览界面点击节点查看 JSON 结构 或查看 `target=JSON` 的通用订阅
|
||||
// 1. `no-resolve` 为不解析域名
|
||||
// 2. 域名解析后 会多一个 `resolved` 字段
|
||||
// 3. 节点字段 `exec` 为 `ssr-local` 路径, 默认 `/usr/local/bin/ssr-local`; 端口从 10000 开始递增(暂不支持配置)
|
||||
|
||||
// $arguments 为传入的脚本参数
|
||||
|
||||
// targetPlatform 为输出的目标平台
|
||||
|
||||
// lodash
|
||||
|
||||
// $substore 为 OpenAPI
|
||||
// 参考 https://github.com/Peng-YM/QuanX/blob/master/Tools/OpenAPI/README.md
|
||||
|
||||
// scriptResourceCache 缓存
|
||||
// 可参考 https://t.me/zhetengsha/1003
|
||||
|
||||
// ProxyUtils 为节点处理工具
|
||||
// 可参考 https://t.me/zhetengsha/1066
|
||||
// const ProxyUtils = {
|
||||
// parse, // 订阅解析
|
||||
// process, // 节点操作/文件操作
|
||||
// produce, // 输出订阅
|
||||
// isIPv4,
|
||||
// isIPv6,
|
||||
// isIP,
|
||||
// yaml, // yaml 解析和生成
|
||||
// }
|
||||
|
||||
// 1. Surge 输出 WireGuard 完整配置
|
||||
|
||||
// let proxies = await produceArtifact({
|
||||
// type: 'subscription',
|
||||
// name: 'sub',
|
||||
// platform: 'Surge',
|
||||
// produceOpts: {
|
||||
// 'include-unsupported-proxy': true,
|
||||
// }
|
||||
// })
|
||||
// $content = proxies
|
||||
|
||||
// 2. sing-box
|
||||
|
||||
// 但是一般不需要这样用, 可参考 1. https://t.me/zhetengsha/1111 和 2. https://t.me/zhetengsha/1070
|
||||
|
||||
// let singboxProxies = await produceArtifact({
|
||||
// type: 'subscription', // type: 'subscription' 或 'collection'
|
||||
// name: 'sub', // subscription name
|
||||
// platform: 'sing-box', // target platform
|
||||
// produceType: 'internal' // 'internal' produces an Array, otherwise produces a String( JSON.parse('JSON String') )
|
||||
// })
|
||||
|
||||
// // JSON
|
||||
// $content = JSON.stringify({}, null, 2)
|
||||
|
||||
// 3. clash.meta
|
||||
|
||||
// 但是一般不需要这样用, 可参考 1. https://t.me/zhetengsha/1111 和 2. https://t.me/zhetengsha/1070
|
||||
|
||||
// let clashMetaProxies = await produceArtifact({
|
||||
// type: 'subscription',
|
||||
// name: 'sub',
|
||||
// platform: 'ClashMeta',
|
||||
// produceType: 'internal' // 'internal' produces an Array, otherwise produces a String( ProxyUtils.yaml.safeLoad('YAML String').proxies )
|
||||
// }))
|
||||
|
||||
// // YAML
|
||||
// $content = ProxyUtils.yaml.safeDump({})
|
||||
|
||||
|
||||
// { $content, $files } will be passed to the next operator
|
||||
// $content is the final content of the file
|
||||
// flowUtils 为机场订阅流量信息处理工具
|
||||
// 可参考 https://t.me/zhetengsha/948
|
||||
// https://github.com/sub-store-org/Sub-Store/blob/31b6dd0507a9286d6ab834ec94ad3050f6bdc86b/backend/src/utils/download.js#L104
|
||||
|
||||
// context 为传入的上下文
|
||||
// 有三种情况, 按需判断
|
||||
|
||||
// 若存在 `source._collection` 且 `source._collection.subscriptions` 中的 key 在 `source` 上也存在, 说明输出结果为组合订阅, 但是脚本设置在单条订阅上
|
||||
|
||||
// 若存在 `source._collection` 但 `source._collection.subscriptions` 中的 key 在 `source` 上不存在, 说明输出结果为组合订阅, 脚本设置在组合订阅上
|
||||
|
||||
// 若不存在 `source._collection`, 说明输出结果为单条订阅, 脚本设置在此单条订阅上
|
||||
|
||||
// 1. 输出单条订阅 sub-1 时, 该单条订阅中的脚本上下文为:
|
||||
// {
|
||||
// "source": {
|
||||
// "sub-1": {
|
||||
// "name": "sub-1",
|
||||
// "displayName": "",
|
||||
// "mergeSources": "",
|
||||
// "ignoreFailedRemoteSub": true,
|
||||
// "process": [],
|
||||
// "icon": "",
|
||||
// "source": "local",
|
||||
// "url": "",
|
||||
// "content": "",
|
||||
// "ua": "",
|
||||
// "display-name": "",
|
||||
// "useCacheForFailedRemoteSub": false
|
||||
// }
|
||||
// },
|
||||
// "backend": "Node",
|
||||
// "version": "2.14.198"
|
||||
// }
|
||||
// 2. 输出组合订阅 collection-1 时, 该组合订阅中的脚本上下文为:
|
||||
// {
|
||||
// "source": {
|
||||
// "_collection": {
|
||||
// "name": "collection-1",
|
||||
// "displayName": "",
|
||||
// "mergeSources": "",
|
||||
// "ignoreFailedRemoteSub": false,
|
||||
// "icon": "",
|
||||
// "process": [],
|
||||
// "subscriptions": [
|
||||
// "sub-1"
|
||||
// ],
|
||||
// "display-name": ""
|
||||
// }
|
||||
// },
|
||||
// "backend": "Node",
|
||||
// "version": "2.14.198"
|
||||
// }
|
||||
// 3. 输出组合订阅 collection-1 时, 该组合订阅中的单条订阅 sub-1 中的某个脚本上下文为:
|
||||
// {
|
||||
// "source": {
|
||||
// "sub-1": {
|
||||
// "name": "sub-1",
|
||||
// "displayName": "",
|
||||
// "mergeSources": "",
|
||||
// "ignoreFailedRemoteSub": true,
|
||||
// "icon": "",
|
||||
// "process": [],
|
||||
// "source": "local",
|
||||
// "url": "",
|
||||
// "content": "",
|
||||
// "ua": "",
|
||||
// "display-name": "",
|
||||
// "useCacheForFailedRemoteSub": false
|
||||
// },
|
||||
// "_collection": {
|
||||
// "name": "collection-1",
|
||||
// "displayName": "",
|
||||
// "mergeSources": "",
|
||||
// "ignoreFailedRemoteSub": false,
|
||||
// "icon": "",
|
||||
// "process": [],
|
||||
// "subscriptions": [
|
||||
// "sub-1"
|
||||
// ],
|
||||
// "display-name": ""
|
||||
// }
|
||||
// },
|
||||
// "backend": "Node",
|
||||
// "version": "2.14.198"
|
||||
// }
|
||||
|
||||
// 参数说明
|
||||
// 可参考 https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E
|
||||
|
||||
console.log(JSON.stringify(context, null, 2))
|
||||
|
||||
return proxies
|
||||
}
|
||||
1
web
1
web
Submodule web deleted from b10b708c34
Reference in New Issue
Block a user