Compare commits

...

18 Commits

Author SHA1 Message Date
xream
5ae6687b1f chore: changelog 2023-08-28 23:15:48 +08:00
xream
ad6d1ab441 fix: build dist 2023-08-28 20:41:40 +08:00
xream
f5aea14904 fix: 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名(不覆盖现有的 Host) 2023-08-28 20:34:22 +08:00
Peng-YM
4f2c95f6ab chore: remove unnecessary files 2023-08-28 20:07:32 +08:00
Peng-YM
be1e2c9979 chore: removed tracking dist files from git 2023-08-28 20:06:50 +08:00
Peng-YM
347b19e30d remove: deprecated artifact 2023-08-28 20:01:05 +08:00
xream
f94a12bf6e feat: bundle 2023-08-28 19:01:34 +08:00
xream
bd510a9aa9 fix: sync 2023-08-28 18:48:33 +08:00
xream
f02af9d643 fix: vless servername 2023-08-28 15:32:08 +08:00
xream
af8e965866 feat: new target platform "Clash.Meta" 2023-08-28 13:10:48 +08:00
xream
4bebcff1d3 feat: 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名 2023-08-28 00:09:24 +08:00
xream
7b8f6f7e52 feat: 域名解析新增 Tencent, Ali; 脚本下载失败, 脚本操作失败, 脚本过滤失败时都会报错了 2023-08-27 23:17:57 +08:00
xream
49c7107d20 fix: transport headers may have no Host 2023-08-27 18:17:30 +08:00
xream
8bfa4dbbf2 feat: VLESS URI 2023-08-27 00:57:21 +08:00
xream
5e14d05c30 feat: 组合订阅错误信息将包含出现错误的子订阅名称; 获取流量失败时, 不影响节点订阅; 订阅上游无有效节点时将报错 2023-08-26 20:27:12 +08:00
xream
8c5dca71fb feat: Loon WireGuard 2023-08-26 15:00:46 +08:00
xream
4973454f58 feat: wireguard 2023-08-25 22:48:03 +08:00
xream
4c6ba2cdc8 feat: hysteria 2023-08-25 16:19:08 +08:00
27 changed files with 741 additions and 134 deletions

View File

@@ -1,15 +1,15 @@
name: build
on:
on:
push:
branches:
- master
paths:
- 'backend/package.json'
- "backend/package.json"
pull_request:
branches:
- master
paths:
- 'backend/package.json'
- "backend/package.json"
jobs:
build:
runs-on: ubuntu-latest
@@ -17,7 +17,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
with:
ref: 'master'
ref: "master"
- name: Set up Node.js
uses: actions/setup-node@v3
with:
@@ -34,17 +34,28 @@ jobs:
run: |
cd backend
pnpm run build
- name: Bundle
run: |
cd backend
pnpm i -D estrella
pnpm run bundle
- id: tag
name: Generate release tag
run: |
cd backend
SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"`
echo "::set-output name=release_tag::$SUBSTORE_RELEASE"
- name: Prepare release
run: |
cd backend
pnpm i -D conventional-changelog-cli
pnpm run changelog
- name: Release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
body_path: ./backend/CHANGELOG.md
tag_name: ${{ steps.tag.outputs.release_tag }}
files: |
./backend/sub-store.min.js
@@ -52,3 +63,4 @@ jobs:
./backend/dist/sub-store-1.min.js
./backend/dist/sub-store-parser.loon.min.js
./backend/dist/cron-sync-artifacts.min.js
./backend/dist/sub-store.bundle.js

7
.gitignore vendored
View File

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

View File

@@ -11,7 +11,7 @@ Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.
</p>
[![Build](https://github.com/Peng-YM/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/Peng-YM/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/Peng-YM/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/Peng-YM/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/Peng-YM/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/Peng-YM/Sub-Store)
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/PengYM)
Core functionalities:
@@ -19,6 +19,8 @@ Core functionalities:
1. Conversion among various formats.
2. Subscription formatting.
3. Collect multiple subscriptions in one URL.
> The following descriptions of features may not be updated in real-time. Please refer to the actual available features for accurate information.
## 1. Subscription Conversion
@@ -29,17 +31,25 @@ Core functionalities:
- [x] SSD URI
- [x] V2RayN URI
- [x] QX (SS, SSR, VMess, Trojan, HTTP)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP)
- [x] Surge (SS, VMess, Trojan, HTTP)
- [x] Stash & Clash (SS, SSR, VMess, Trojan, HTTP)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, WireGuard, VLESS)
- [x] Surge (SS, VMess, Trojan, HTTP, TUIC, Snell)
- [x] ShadowRocket (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria)
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria)
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria)
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, Snell)
### Supported Target Platforms
- [x] QX
- [x] Loon
- [x] Surge
- [x] Stash & Clash
- [x] Stash
- [x] Clash.Meta
- [x] Clash
- [x] ShadowRocket
- [x] V2Ray
- [x] V2Ray URI
- [x] Plain JSON
## 2. Subscription Formatting
@@ -61,6 +71,7 @@ Core functionalities:
- [x] **Regex rename operator**: replace by regex in proxy names.
- [x] **Regex delete operator**: delete by regex in proxy names.
- [x] **Script operator**: modify proxy by script.
- [x] **Resolve Domain Operator**: resolve the domain of nodes to an IP address.
### Development

23
backend/bundle.js Normal file
View File

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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.14.26",
"version": "2.14.41",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js",
"scripts": {
@@ -8,7 +8,9 @@
"test": "gulp peggy && npx cross-env BABEL_ENV=test mocha src/test/**/*.spec.js --require @babel/register --recursive",
"serve": "node sub-store.min.js",
"start": "nodemon -w src -w package.json --exec babel-node src/main.js",
"build": "gulp"
"build": "gulp",
"bundle": "node bundle.js",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
},
"author": "Peng-YM",
"license": "GPL-3.0",

View File

@@ -90,8 +90,7 @@ async function process(proxies, operators = [], targetPlatform) {
$.error(
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`,
);
// skip the script if download failed.
continue;
throw new Error(`无法下载脚本: ${url}`);
}
} else {
script = content;
@@ -188,6 +187,8 @@ function lastParse(proxy) {
if (proxy.network === 'tcp') {
delete proxy.network;
}
}
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) {
proxy.tls = true;
}
if (proxy.tls && !proxy.sni) {
@@ -204,6 +205,19 @@ function lastParse(proxy) {
proxy.sni = proxy.server;
}
}
// 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名(不覆盖现有的 Host)
if (
!proxy.tls &&
['ws', 'http'].includes(proxy.network) &&
!proxy[`${proxy.network}-opts`]?.headers?.Host &&
!isIP(proxy.server)
) {
proxy[`${proxy.network}-opts`] = proxy[`${proxy.network}-opts`] || {};
proxy[`${proxy.network}-opts`].headers =
proxy[`${proxy.network}-opts`].headers || {};
proxy[`${proxy.network}-opts`].headers.Host =
proxy.network === 'http' ? [proxy.server] : proxy.server;
}
return proxy;
}

View File

@@ -306,6 +306,85 @@ function URI_VMess() {
return { name, test, parse };
}
function URI_VLESS() {
const name = 'URI VLESS Parser';
const test = (line) => {
return /^vless:\/\//.test(line);
};
const parse = (line) => {
line = line.split('vless://')[1];
// eslint-disable-next-line no-unused-vars
let [__, uuid, server, port, addons, name] =
/^(.*?)@(.*?):(\d+)\/?\?(.*?)(?:#(.*?))$/.exec(line);
port = parseInt(`${port}`, 10);
uuid = decodeURIComponent(uuid);
name = decodeURIComponent(name) ?? `VLESS ${server}:${port}`;
const proxy = {
type: 'vless',
name,
server,
port,
uuid,
};
const params = {};
for (const addon of addons.split('&')) {
const [key, valueRaw] = addon.split('=');
let value = valueRaw;
value = decodeURIComponent(valueRaw);
params[key] = value;
}
proxy.tls = params.security && params.security !== 'none';
proxy.sni = params.sni;
proxy.flow = params.flow;
proxy['client-fingerprint'] = params.fp;
proxy.alpn = params.alpn ? params.alpn.split(',') : undefined;
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure);
if (['reality'].includes(params.security)) {
const opts = {};
if (params.pbk) {
opts['public-key'] = params.pbk;
}
if (params.sid) {
opts['short-id'] = params.sid;
}
if (Object.keys(opts).length > 0) {
proxy[`${params.security}-opts`] = opts;
}
}
proxy.network = params.type;
if (proxy.network && !['tcp', 'none'].includes(proxy.network)) {
const opts = {};
if (params.path) {
opts.path = params.path;
}
if (params.host) {
opts.headers = { Host: params.host };
}
if (params.serviceName) {
opts[`${proxy.network}-service-name`] = params.serviceName;
}
if (Object.keys(opts).length > 0) {
proxy[`${proxy.network}-opts`] = opts;
}
}
if (proxy.tls && !proxy.sni) {
if (proxy.network === 'ws') {
proxy.sni = proxy['ws-opts']?.headers?.Host;
} else if (proxy.network === 'http') {
let httpHost = proxy['http-opts']?.headers?.Host;
proxy.sni = Array.isArray(httpHost) ? httpHost[0] : httpHost;
}
}
return proxy;
};
return { name, test, parse };
}
// Trojan URI format
function URI_Trojan() {
const name = 'URI Trojan Parser';
@@ -344,6 +423,8 @@ function Clash_All() {
'trojan',
'tuic',
'vless',
'hysteria',
'wireguard',
].includes(proxy.type)
) {
throw new Error(
@@ -501,6 +582,113 @@ function Loon_Http() {
return { name, test, parse };
}
function Loon_WireGuard() {
const name = 'Loon WireGuard Parser';
const test = (line) => {
return /^.*=\s*wireguard/i.test(line.split(',')[0]);
};
const parse = (line) => {
const name = line.match(
/(^.*?)\s*?=\s*?wireguard\s*?,.+?\s*?=\s*?.+?/i,
)?.[1];
line = line.replace(name, '').replace(/^\s*?=\s*?wireguard\s*/i, '');
let peers = line.match(
/,\s*?peers\s*?=\s*?\[\s*?\{\s*?(.+?)\s*?\}\s*?\]/i,
)?.[1];
let serverPort = peers.match(
/(,|^)\s*?endpoint\s*?=\s*?"?(.+?):(\d+)"?\s*?(,|$)/i,
);
let server = serverPort?.[2];
let port = parseInt(serverPort?.[3], 10);
let mtu = line.match(/(,|^)\s*?mtu\s*?=\s*?"?(\d+?)"?\s*?(,|$)/i)?.[2];
if (mtu) {
mtu = parseInt(mtu, 10);
}
let keepalive = line.match(
/(,|^)\s*?keepalive\s*?=\s*?"?(\d+?)"?\s*?(,|$)/i,
)?.[2];
if (keepalive) {
keepalive = parseInt(keepalive, 10);
}
let reserved = peers.match(
/(,|^)\s*?reserved\s*?=\s*?"?(\[\s*?.+?\s*?\])"?\s*?(,|$)/i,
)?.[2];
if (reserved) {
reserved = JSON.parse(reserved);
}
let dns;
let dnsv4 = line.match(/(,|^)\s*?dns\s*?=\s*?"?(.+?)"?\s*?(,|$)/i)?.[2];
let dnsv6 = line.match(
/(,|^)\s*?dnsv6\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
if (dnsv4 || dnsv6) {
dns = [];
if (dnsv4) {
dns.push(dnsv4);
}
if (dnsv6) {
dns.push(dnsv6);
}
}
let allowedIps = peers
.match(/(,|^)\s*?allowed-ips\s*?=\s*?"(.+?)"\s*?(,|$)/i)?.[2]
?.split(',')
.map((i) => i.trim());
let preSharedKey = peers.match(
/(,|^)\s*?preshared-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
let ip = line.match(
/(,|^)\s*?interface-ip\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
let ipv6 = line.match(
/(,|^)\s*?interface-ipv6\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
let publicKey = peers.match(
/(,|^)\s*?public-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
const proxy = {
type: 'wireguard',
name,
server,
port,
ip,
ipv6,
'private-key': line.match(
/(,|^)\s*?private-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2],
'public-key': publicKey,
mtu,
keepalive,
reserved,
'allowed-ips': allowedIps,
'preshared-key': preSharedKey,
dns,
udp: true,
peers: [
{
server,
port,
ip,
ipv6,
'public-key': publicKey,
'pre-shared-key': preSharedKey,
allowed_ips: allowedIps,
reserved,
},
],
};
proxy;
if (Array.isArray(proxy.dns) && proxy.dns.length > 0) {
proxy['remote-dns-resolve'] = true;
}
return proxy;
};
return { name, test, parse };
}
function Surge_SS() {
const name = 'Surge SS Parser';
const test = (line) => {
@@ -571,6 +759,7 @@ export default [
URI_SS(),
URI_SSR(),
URI_VMess(),
URI_VLESS(),
URI_Trojan(),
Clash_All(),
Surge_SS(),
@@ -586,6 +775,7 @@ export default [
Loon_Vless(),
Loon_Trojan(),
Loon_Http(),
Loon_WireGuard(),
QX_SS(),
QX_SSR(),
QX_VMess(),

View File

@@ -378,6 +378,47 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result);
return result;
},
Ali: async function (domain) {
const id = hex_md5(`ALI:${domain}`);
const cached = resourceCache.get(id);
if (cached) return cached;
const resp = await $.http.get({
url: `http://223.6.6.6/resolve?name=${encodeURIComponent(
domain,
)}&type=A&short=1`,
headers: {
accept: 'application/dns-json',
},
});
const answers = JSON.parse(resp.body);
if (answers.length === 0) {
throw new Error('No answers');
}
const result = answers[answers.length - 1];
resourceCache.set(id, result);
return result;
},
Tencent: async function (domain) {
const id = hex_md5(`ALI:${domain}`);
const cached = resourceCache.get(id);
if (cached) return cached;
const resp = await $.http.get({
url: `http://119.28.28.28/d?type=A&dn=${encodeURIComponent(
domain,
)}`,
headers: {
accept: 'application/dns-json',
},
});
const answers = resp.body.split(';').map((i) => i.split(',')[0]);
console.log(`answers`, answers);
if (answers.length === 0) {
throw new Error('No answers');
}
const result = answers[answers.length - 1];
resourceCache.set(id, result);
return result;
},
};
function ResolveDomainOperator({ provider }) {
@@ -566,7 +607,8 @@ async function ApplyFilter(filter, objs) {
selected = await filter.func(objs);
} catch (err) {
// print log and skip this filter
$.log(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
$.error(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
throw new Error(`脚本过滤失败 ${err.message ?? err}`);
}
return objs.filter((_, i) => selected[i]);
}
@@ -578,7 +620,8 @@ async function ApplyOperator(operator, objs) {
if (output_) output = output_;
} catch (err) {
// print log and skip this operator
$.log(`Cannot apply operator ${operator.name}! Reason: ${err}`);
$.error(`Cannot apply operator ${operator.name}! Reason: ${err}`);
throw new Error(`脚本操作失败 ${err.message ?? err}`);
}
return output;
}

View File

@@ -14,6 +14,7 @@ export default function Clash_Producer() {
'http',
'snell',
'trojan',
'wireguard',
].includes(proxy.type)
) {
return false;
@@ -53,6 +54,18 @@ export default function Clash_Producer() {
) {
proxy.cipher = 'auto';
}
} else if (proxy.type === 'wireguard') {
proxy.keepalive =
proxy.keepalive ?? proxy['persistent-keepalive'];
proxy['persistent-keepalive'] = proxy.keepalive;
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
}
if (
@@ -74,7 +87,9 @@ export default function Clash_Producer() {
proxy['http-opts'].headers.Host = [httpHost];
}
}
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) {
delete proxy.tls;
}
delete proxy['tls-fingerprint'];
return ' - ' + JSON.stringify(proxy) + '\n';
})

View File

@@ -0,0 +1,117 @@
import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function ClashMeta_Producer() {
const type = 'ALL';
const produce = (proxies) => {
return (
'proxies:\n' +
proxies
.filter((proxy) => {
if (
proxy.type === 'snell' &&
String(proxy.version) === '4'
) {
return false;
}
return true;
})
.map((proxy) => {
if (proxy.type === 'vmess') {
// handle vmess aead
if (isPresent(proxy, 'aead')) {
if (proxy.aead) {
proxy.alterId = 0;
}
delete proxy.aead;
}
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400
// https://stash.wiki/proxy-protocols/proxy-types#vmess
if (
isPresent(proxy, 'cipher') &&
![
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(proxy.cipher)
) {
proxy.cipher = 'auto';
}
} else if (proxy.type === 'tuic') {
if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn];
} else {
proxy.alpn = ['h3'];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
if (
(!proxy.token || proxy.token.length === 0) &&
!isPresent(proxy, 'version')
) {
proxy.version = 5;
}
} else if (proxy.type === 'hysteria') {
if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
}
} else if (proxy.type === 'wireguard') {
proxy.keepalive =
proxy.keepalive ?? proxy['persistent-keepalive'];
proxy['persistent-keepalive'] = proxy.keepalive;
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
}
if (
['vmess', 'vless'].includes(proxy.type) &&
proxy.network === 'http'
) {
let httpPath = proxy['http-opts']?.path;
if (
isPresent(proxy, 'http-opts.path') &&
!Array.isArray(httpPath)
) {
proxy['http-opts'].path = [httpPath];
}
let httpHost = proxy['http-opts']?.headers?.Host;
if (
isPresent(proxy, 'http-opts.headers.Host') &&
!Array.isArray(httpHost)
) {
proxy['http-opts'].headers.Host = [httpHost];
}
}
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) {
delete proxy.tls;
}
delete proxy['tls-fingerprint'];
return ' - ' + JSON.stringify(proxy) + '\n';
})
.join('')
);
};
return { type, produce };
}

View File

@@ -1,5 +1,6 @@
import Surge_Producer from './surge';
import Clash_Producer from './clash';
import ClashMeta_Producer from './clashmeta';
import Stash_Producer from './stash';
import Loon_Producer from './loon';
import URI_Producer from './uri';
@@ -18,6 +19,7 @@ export default {
Surge: Surge_Producer(),
Loon: Loon_Producer(),
Clash: Clash_Producer(),
ClashMeta: ClashMeta_Producer(),
URI: URI_Producer(),
V2Ray: V2Ray_Producer(),
JSON: JSON_Producer(),

View File

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

View File

@@ -135,11 +135,11 @@ function trojan(proxy) {
if (needTls(proxy)) append(`,obfs=wss`);
else append(`,obfs=ws`);
appendIfPresent(
`,obfs-uri=${proxy['ws-opts'].path}`,
`,obfs-uri=${proxy['ws-opts']?.path}`,
'ws-opts.path',
);
appendIfPresent(
`,obfs-host=${proxy['ws-opts'].headers.Host}`,
`,obfs-host=${proxy['ws-opts']?.headers?.Host}`,
'ws-opts.headers.Host',
);
} else {

View File

@@ -1,6 +1,6 @@
import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function Stash_Producer() {
export default function ShadowRocket_Producer() {
const type = 'ALL';
const produce = (proxies) => {
return (
@@ -49,6 +49,12 @@ export default function Stash_Producer() {
} else {
proxy.alpn = ['h3'];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
if (
(!proxy.token || proxy.token.length === 0) &&
@@ -56,6 +62,30 @@ export default function Stash_Producer() {
) {
proxy.version = 5;
}
} else if (proxy.type === 'hysteria') {
if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
}
} else if (proxy.type === 'wireguard') {
proxy.keepalive =
proxy.keepalive ?? proxy['persistent-keepalive'];
proxy['persistent-keepalive'] = proxy.keepalive;
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
}
if (
@@ -78,6 +108,10 @@ export default function Stash_Producer() {
}
}
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) {
delete proxy.tls;
}
delete proxy['tls-fingerprint'];
return ' - ' + JSON.stringify(proxy) + '\n';
})

View File

@@ -18,6 +18,7 @@ export default function Stash_Producer() {
'trojan',
'tuic',
'vless',
'wireguard',
].includes(proxy.type) ||
(proxy.type === 'snell' &&
String(proxy.version) === '4') ||
@@ -61,6 +62,12 @@ export default function Stash_Producer() {
} else {
proxy.alpn = ['h3'];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
if (
(!proxy.token || proxy.token.length === 0) &&
@@ -68,6 +75,30 @@ export default function Stash_Producer() {
) {
proxy.version = 5;
}
} else if (proxy.type === 'hysteria') {
if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
}
} else if (proxy.type === 'wireguard') {
proxy.keepalive =
proxy.keepalive ?? proxy['persistent-keepalive'];
proxy['persistent-keepalive'] = proxy.keepalive;
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
}
if (
@@ -89,7 +120,9 @@ export default function Stash_Producer() {
proxy['http-opts'].headers.Host = [httpHost];
}
}
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) {
delete proxy.tls;
}
delete proxy['tls-fingerprint'];
return ' - ' + JSON.stringify(proxy) + '\n';
})

View File

@@ -94,6 +94,89 @@ export default function URI_Producer() {
}
result = 'vmess://' + Base64.encode(JSON.stringify(result));
break;
case 'vless':
let security = 'none';
const isReality = proxy['reality-opts'];
let sid = '';
let pbk = '';
if (isReality) {
security = 'reality';
const publicKey = proxy['reality-opts']?.['public-key'];
if (publicKey) {
pbk = `&pbk=${encodeURIComponent(publicKey)}`;
}
const shortId = proxy['reality-opts']?.['short-id'];
if (shortId) {
sid = `&sid=${encodeURIComponent(shortId)}`;
}
} else if (proxy.tls) {
security = 'tls';
}
let alpn = '';
if (proxy.alpn) {
alpn = `&alpn=${encodeURIComponent(
Array.isArray(proxy.alpn)
? proxy.alpn
: proxy.alpn.join(','),
)}`;
}
let allowInsecure = '';
if (proxy['skip-cert-verify']) {
allowInsecure = `&allowInsecure=1`;
}
let sni = '';
if (proxy.sni) {
sni = `&sni=${encodeURIComponent(proxy.sni)}`;
}
let fp = '';
if (proxy['client-fingerprint']) {
fp = `&fp=${encodeURIComponent(
proxy['client-fingerprint'],
)}`;
}
let flow = '';
if (proxy.flow) {
flow = `&flow=${encodeURIComponent(proxy.flow)}`;
}
let vlessTransport = `&type=${encodeURIComponent(
proxy.network,
)}`;
let vlessTransportServiceName =
proxy[`${proxy.network}-opts`]?.[
`${proxy.network}-service-name`
];
let vlessTransportPath = proxy[`${proxy.network}-opts`]?.path;
let vlessTransportHost =
proxy[`${proxy.network}-opts`]?.headers?.Host;
if (vlessTransportPath) {
vlessTransport += `&path=${encodeURIComponent(
Array.isArray(vlessTransportPath)
? vlessTransportPath[0]
: vlessTransportPath,
)}`;
}
if (vlessTransportHost) {
vlessTransport += `&host=${encodeURIComponent(
Array.isArray(vlessTransportHost)
? vlessTransportHost[0]
: vlessTransportHost,
)}`;
}
if (vlessTransportServiceName) {
vlessTransport += `&serviceName=${encodeURIComponent(
vlessTransportServiceName,
)}`;
}
result = `vless://${proxy.uuid}@${proxy.server}:${
proxy.port
}?${vlessTransport}&security=${encodeURIComponent(
security,
)}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${pbk}#${encodeURIComponent(
proxy.name,
)}`;
break;
case 'trojan':
let trojanTransport = '';
if (proxy.network) {

View File

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

View File

@@ -136,7 +136,7 @@ async function gistBackup(req, res) {
new InternalServerError(
'BACKUP_FAILED',
`Failed to ${action} data to gist!`,
`Reason: ${JSON.stringify(err)}`,
`Reason: ${err.message ?? err}`,
),
);
}

View File

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

View File

@@ -1,9 +1,11 @@
export function getPlatformFromHeaders(headers) {
const keys = Object.keys(headers);
let UA = '';
let ua = '';
for (let k of keys) {
if (/USER-AGENT/i.test(k)) {
UA = headers[k];
ua = UA.toLowerCase();
break;
}
}
@@ -17,6 +19,15 @@ export function getPlatformFromHeaders(headers) {
return 'ShadowRocket';
} else if (UA.indexOf('Stash') !== -1) {
return 'Stash';
} else if (
ua === 'meta' ||
(ua.indexOf('clash') !== -1 && ua.indexOf('meta') !== -1)
) {
return 'ClashMeta';
} else if (ua.indexOf('clash') !== -1) {
return 'Clash';
} else if (ua.indexOf('v2ray') !== -1) {
return 'V2Ray';
} else {
return 'JSON';
}

View File

@@ -48,7 +48,7 @@ class ResourceCache {
}
set(id, value) {
this.resourceCache[id] = { time: new Date().getTime(), data: value }
this.resourceCache[id] = { time: new Date().getTime(), data: value };
this._persist();
}
}

File diff suppressed because one or more lines are too long