mirror of
https://git.mirrors.martin98.com/https://github.com/sub-store-org/Sub-Store.git
synced 2026-03-24 03:43:19 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f02af9d643 | ||
|
|
af8e965866 | ||
|
|
4bebcff1d3 | ||
|
|
7b8f6f7e52 | ||
|
|
49c7107d20 |
21
README.md
21
README.md
@@ -11,7 +11,7 @@ Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.
|
||||
</p>
|
||||
|
||||
[](https://github.com/Peng-YM/Sub-Store/actions/workflows/main.yml)     
|
||||
|
||||
|
||||
[](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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sub-store",
|
||||
"version": "2.14.31",
|
||||
"version": "2.14.36",
|
||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -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;
|
||||
@@ -206,6 +205,17 @@ function lastParse(proxy) {
|
||||
proxy.sni = proxy.server;
|
||||
}
|
||||
}
|
||||
// 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名
|
||||
if (
|
||||
!proxy.tls &&
|
||||
['ws', 'http'].includes(proxy.network) &&
|
||||
!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.server;
|
||||
}
|
||||
return proxy;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,11 @@ export default function Clash_Producer() {
|
||||
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 (
|
||||
|
||||
117
backend/src/core/proxy-utils/producers/clashmeta.js
Normal file
117
backend/src/core/proxy-utils/producers/clashmeta.js
Normal 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 };
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -101,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 {
|
||||
@@ -138,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') {
|
||||
@@ -205,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') {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
@@ -81,6 +81,11 @@ export default function Stash_Producer() {
|
||||
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 (
|
||||
|
||||
@@ -94,6 +94,11 @@ export default function Stash_Producer() {
|
||||
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 (
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user