mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b3b9177e5 | ||
|
|
91aab3ca7a | ||
|
|
c1a9fc6abc | ||
|
|
11d9ce7372 | ||
|
|
ad3d2270ac | ||
|
|
3ad42f2c10 | ||
|
|
ec06eb8659 | ||
|
|
4a23a4d8b6 | ||
|
|
913638a233 | ||
|
|
bf642ce0e6 | ||
|
|
1ecac9da92 | ||
|
|
c5a417da8f | ||
|
|
8cd0545023 | ||
|
|
b6f848a6e6 | ||
|
|
99d058bcf1 | ||
|
|
533103e765 | ||
|
|
cf82764171 | ||
|
|
7b783c1fe3 | ||
|
|
372eff9a44 | ||
|
|
d3b5a529d7 | ||
|
|
8049134bb5 | ||
|
|
3f620700a4 | ||
|
|
9e64a68481 | ||
|
|
9ce5916414 | ||
|
|
047c21fe70 |
@@ -35,7 +35,7 @@ Core functionalities:
|
||||
- [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] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC, Juicity, SSH)
|
||||
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)
|
||||
|
||||
### Supported Target Platforms
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sub-store",
|
||||
"version": "2.14.321",
|
||||
"version": "2.14.342",
|
||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
@@ -25,6 +25,7 @@
|
||||
"express": "^4.17.1",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"js-base64": "^3.7.2",
|
||||
"jsrsasign": "^11.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"request": "^2.88.2",
|
||||
"requests": "^0.3.0",
|
||||
|
||||
9
backend/pnpm-lock.yaml
generated
9
backend/pnpm-lock.yaml
generated
@@ -29,6 +29,9 @@ dependencies:
|
||||
js-base64:
|
||||
specifier: ^3.7.2
|
||||
version: registry.npmmirror.com/js-base64@3.7.2
|
||||
jsrsasign:
|
||||
specifier: ^11.1.0
|
||||
version: registry.npmmirror.com/jsrsasign@11.1.0
|
||||
lodash:
|
||||
specifier: ^4.17.21
|
||||
version: registry.npmmirror.com/lodash@4.17.21
|
||||
@@ -6634,6 +6637,12 @@ packages:
|
||||
verror: registry.npmmirror.com/verror@1.10.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/jsrsasign@11.1.0:
|
||||
resolution: {integrity: sha512-Ov74K9GihaK9/9WncTe1mPmvrO7Py665TUfUKvraXBpu+xcTWitrtuOwcjf4KMU9maPaYn0OuaWy0HOzy/GBXg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/jsrsasign/-/jsrsasign-11.1.0.tgz}
|
||||
name: jsrsasign
|
||||
version: 11.1.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/just-debounce@1.1.0:
|
||||
resolution: {integrity: sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/just-debounce/-/just-debounce-1.1.0.tgz}
|
||||
name: just-debounce
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import rs from '@/utils/rs';
|
||||
import YAML from '@/utils/yaml';
|
||||
import download from '@/utils/download';
|
||||
import {
|
||||
@@ -15,7 +16,7 @@ import $ from '@/core/app';
|
||||
import { FILES_KEY, MODULES_KEY } from '@/constants';
|
||||
import { findByName } from '@/utils/database';
|
||||
import { produceArtifact } from '@/restful/sync';
|
||||
import { getFlag, getISO, MMDB } from '@/utils/geo';
|
||||
import { getFlag, removeFlag, getISO, MMDB } from '@/utils/geo';
|
||||
import Gist from '@/utils/gist';
|
||||
|
||||
function preprocess(raw) {
|
||||
@@ -83,7 +84,7 @@ async function processFn(proxies, operators = [], targetPlatform, source) {
|
||||
const { mode, content } = item.args;
|
||||
if (mode === 'link') {
|
||||
let noCache;
|
||||
let url = content;
|
||||
let url = content || '';
|
||||
if (url.endsWith('#noCache')) {
|
||||
url = url.replace(/#noCache$/, '');
|
||||
noCache = true;
|
||||
@@ -276,6 +277,7 @@ export const ProxyUtils = {
|
||||
isIP,
|
||||
yaml: YAML,
|
||||
getFlag,
|
||||
removeFlag,
|
||||
getISO,
|
||||
MMDB,
|
||||
Gist,
|
||||
@@ -337,7 +339,11 @@ function lastParse(proxy) {
|
||||
proxy.network = 'tcp';
|
||||
}
|
||||
}
|
||||
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity'].includes(
|
||||
proxy.type,
|
||||
)
|
||||
) {
|
||||
proxy.tls = true;
|
||||
}
|
||||
if (proxy.network) {
|
||||
@@ -437,9 +443,46 @@ function lastParse(proxy) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (['ws', 'http', 'h2'].includes(proxy.network)) {
|
||||
if (
|
||||
['ws', 'h2'].includes(proxy.network) &&
|
||||
!proxy[`${proxy.network}-opts`]?.path
|
||||
) {
|
||||
proxy[`${proxy.network}-opts`] =
|
||||
proxy[`${proxy.network}-opts`] || {};
|
||||
proxy[`${proxy.network}-opts`].path = '/';
|
||||
} else if (
|
||||
proxy.network === 'http' &&
|
||||
(!Array.isArray(proxy[`${proxy.network}-opts`]?.path) ||
|
||||
proxy[`${proxy.network}-opts`]?.path.every((i) => !i))
|
||||
) {
|
||||
proxy[`${proxy.network}-opts`] =
|
||||
proxy[`${proxy.network}-opts`] || {};
|
||||
proxy[`${proxy.network}-opts`].path = ['/'];
|
||||
}
|
||||
}
|
||||
if (['', 'off'].includes(proxy.sni)) {
|
||||
proxy['disable-sni'] = true;
|
||||
}
|
||||
let caStr = proxy['ca_str'];
|
||||
if (proxy['ca-str']) {
|
||||
caStr = proxy['ca-str'];
|
||||
} else if (caStr) {
|
||||
delete proxy['ca_str'];
|
||||
proxy['ca-str'] = caStr;
|
||||
}
|
||||
try {
|
||||
if ($.env.isNode && !caStr && proxy['_ca']) {
|
||||
caStr = $.node.fs.readFileSync(proxy['_ca'], {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
$.error(`Read ca file failed\nReason: ${e}`);
|
||||
}
|
||||
if (!proxy['tls-fingerprint'] && caStr) {
|
||||
proxy['tls-fingerprint'] = rs.generateFingerprint(caStr);
|
||||
}
|
||||
return proxy;
|
||||
}
|
||||
|
||||
|
||||
@@ -305,8 +305,9 @@ function URI_VMess() {
|
||||
if (params.net === 'ws' || params.obfs === 'websocket') {
|
||||
proxy.network = 'ws';
|
||||
} else if (
|
||||
['tcp', 'http'].includes(params.net) ||
|
||||
params.obfs === 'http'
|
||||
['http'].includes(params.net) ||
|
||||
['http'].includes(params.obfs) ||
|
||||
['http'].includes(params.type)
|
||||
) {
|
||||
proxy.network = 'http';
|
||||
} else if (['grpc'].includes(params.net)) {
|
||||
@@ -317,6 +318,8 @@ function URI_VMess() {
|
||||
) {
|
||||
proxy.network = 'ws';
|
||||
httpupgrade = true;
|
||||
} else if (params.net === 'h2' || proxy.network === 'h2') {
|
||||
proxy.network = 'h2';
|
||||
}
|
||||
if (proxy.network) {
|
||||
let transportHost = params.host ?? params.obfsParam;
|
||||
@@ -332,6 +335,10 @@ function URI_VMess() {
|
||||
|
||||
if (proxy.network === 'http') {
|
||||
if (transportHost) {
|
||||
// 1)http(tcp)->host中间逗号(,)隔开
|
||||
transportHost = transportHost
|
||||
.split(',')
|
||||
.map((i) => i.trim());
|
||||
transportHost = Array.isArray(transportHost)
|
||||
? transportHost[0]
|
||||
: transportHost;
|
||||
@@ -340,6 +347,8 @@ function URI_VMess() {
|
||||
transportPath = Array.isArray(transportPath)
|
||||
? transportPath[0]
|
||||
: transportPath;
|
||||
} else {
|
||||
transportPath = '/';
|
||||
}
|
||||
}
|
||||
if (transportPath || transportHost) {
|
||||
|
||||
@@ -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/fast_open/download_bandwidth/ecn/others)* {
|
||||
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/others)* {
|
||||
proxy.type = "hysteria2";
|
||||
}
|
||||
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
@@ -178,6 +178,7 @@ udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
||||
|
||||
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
|
||||
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
|
||||
salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join(""); proxy.obfs = 'salamander'; }
|
||||
|
||||
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
|
||||
comma = _ "," _
|
||||
|
||||
@@ -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/fast_open/download_bandwidth/ecn/others)* {
|
||||
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/others)* {
|
||||
proxy.type = "hysteria2";
|
||||
}
|
||||
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
@@ -176,6 +176,7 @@ udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
||||
|
||||
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
|
||||
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
|
||||
salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join(""); proxy.obfs = 'salamander'; }
|
||||
|
||||
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
|
||||
comma = _ "," _
|
||||
|
||||
@@ -2,7 +2,7 @@ import resourceCache from '@/utils/resource-cache';
|
||||
import scriptResourceCache from '@/utils/script-resource-cache';
|
||||
import { isIPv4, isIPv6 } from '@/utils';
|
||||
import { FULL } from '@/utils/logical';
|
||||
import { getFlag } from '@/utils/geo';
|
||||
import { getFlag, removeFlag } from '@/utils/geo';
|
||||
import lodash from 'lodash';
|
||||
import $ from '@/core/app';
|
||||
import { hex_md5 } from '@/vendor/md5';
|
||||
@@ -406,10 +406,13 @@ const DOMAIN_RESOLVERS = {
|
||||
throw new Error(`Status is ${body['Status']}`);
|
||||
}
|
||||
const answers = body['Answer'];
|
||||
if (answers.length === 0) {
|
||||
if (!Array.isArray(answers) || answers.length === 0) {
|
||||
throw new Error('No answers');
|
||||
}
|
||||
const result = answers.map((i) => i?.data).filter((i) => i);
|
||||
if (result.length === 0) {
|
||||
throw new Error('No answers');
|
||||
}
|
||||
const result = answers[answers.length - 1].data;
|
||||
resourceCache.set(id, result);
|
||||
return result;
|
||||
},
|
||||
@@ -429,7 +432,13 @@ const DOMAIN_RESOLVERS = {
|
||||
if (body['status'] !== 'success') {
|
||||
throw new Error(`Status is ${body['status']}`);
|
||||
}
|
||||
const result = body.query;
|
||||
if (!body.query || body.query === 0) {
|
||||
throw new Error('No answers');
|
||||
}
|
||||
const result = [body.query];
|
||||
if (result.length === 0) {
|
||||
throw new Error('No answers');
|
||||
}
|
||||
resourceCache.set(id, result);
|
||||
return result;
|
||||
},
|
||||
@@ -450,10 +459,13 @@ const DOMAIN_RESOLVERS = {
|
||||
throw new Error(`Status is ${body['Status']}`);
|
||||
}
|
||||
const answers = body['Answer'];
|
||||
if (answers.length === 0) {
|
||||
if (!Array.isArray(answers) || answers.length === 0) {
|
||||
throw new Error('No answers');
|
||||
}
|
||||
const result = answers.map((i) => i?.data).filter((i) => i);
|
||||
if (result.length === 0) {
|
||||
throw new Error('No answers');
|
||||
}
|
||||
const result = answers[answers.length - 1].data;
|
||||
resourceCache.set(id, result);
|
||||
return result;
|
||||
},
|
||||
@@ -470,15 +482,18 @@ const DOMAIN_RESOLVERS = {
|
||||
},
|
||||
});
|
||||
const answers = JSON.parse(resp.body);
|
||||
if (answers.length === 0) {
|
||||
if (!Array.isArray(answers) || answers.length === 0) {
|
||||
throw new Error('No answers');
|
||||
}
|
||||
const result = answers;
|
||||
if (result.length === 0) {
|
||||
throw new Error('No answers');
|
||||
}
|
||||
const result = answers[answers.length - 1];
|
||||
resourceCache.set(id, result);
|
||||
return result;
|
||||
},
|
||||
Tencent: async function (domain, type, noCache) {
|
||||
const id = hex_md5(`ALI:${domain}:${type}`);
|
||||
const id = hex_md5(`TENCENT:${domain}:${type}`);
|
||||
const cached = resourceCache.get(id);
|
||||
if (!noCache && cached) return cached;
|
||||
const resp = await $.http.get({
|
||||
@@ -493,7 +508,10 @@ const DOMAIN_RESOLVERS = {
|
||||
if (answers.length === 0 || String(answers) === '0') {
|
||||
throw new Error('No answers');
|
||||
}
|
||||
const result = answers[answers.length - 1];
|
||||
const result = answers;
|
||||
if (result.length === 0) {
|
||||
throw new Error('No answers');
|
||||
}
|
||||
resourceCache.set(id, result);
|
||||
return result;
|
||||
},
|
||||
@@ -512,12 +530,17 @@ function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
|
||||
return {
|
||||
name: 'Resolve Domain Operator',
|
||||
func: async (proxies) => {
|
||||
proxies.forEach((p, i) => {
|
||||
if (!p['_no-resolve'] && p['no-resolve']) {
|
||||
proxies[i]['_no-resolve'] = p['no-resolve'];
|
||||
}
|
||||
});
|
||||
const results = {};
|
||||
const limit = 15; // more than 20 concurrency may result in surge TCP connection shortage.
|
||||
const totalDomain = [
|
||||
...new Set(
|
||||
proxies
|
||||
.filter((p) => !isIP(p.server) && !p['no-resolve'])
|
||||
.filter((p) => !isIP(p.server) && !p['_no-resolve'])
|
||||
.map((c) => c.server),
|
||||
),
|
||||
];
|
||||
@@ -543,12 +566,18 @@ function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
|
||||
await Promise.all(currentBatch);
|
||||
}
|
||||
proxies.forEach((p) => {
|
||||
if (!p['no-resolve']) {
|
||||
if (!p['_no-resolve']) {
|
||||
if (results[p.server]) {
|
||||
p._resolved_ips = results[p.server];
|
||||
const ip = Array.isArray(results[p.server])
|
||||
? results[p.server][
|
||||
Math.floor(
|
||||
Math.random() * results[p.server].length,
|
||||
)
|
||||
]
|
||||
: results[p.server];
|
||||
if (_type === 'IP4P') {
|
||||
const { server, port } = parseIP4P(
|
||||
results[p.server],
|
||||
);
|
||||
const { server, port } = parseIP4P(ip);
|
||||
if (server && port) {
|
||||
p._domain = p.server;
|
||||
p.server = server;
|
||||
@@ -563,7 +592,7 @@ function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
|
||||
}
|
||||
} else {
|
||||
p._domain = p.server;
|
||||
p.server = results[p.server];
|
||||
p.server = ip;
|
||||
p.resolved = true;
|
||||
p[`_${type}`] = p.server;
|
||||
if (!isIP(p._IP)) {
|
||||
@@ -578,7 +607,7 @@ function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
|
||||
|
||||
return proxies.filter((p) => {
|
||||
if (filter === 'removeFailed') {
|
||||
return isIP(p.server) || p['no-resolve'] || p.resolved;
|
||||
return isIP(p.server) || p['_no-resolve'] || p.resolved;
|
||||
} else if (filter === 'IPOnly') {
|
||||
return isIP(p.server);
|
||||
} else if (filter === 'IPv4Only') {
|
||||
@@ -864,13 +893,6 @@ function clone(object) {
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
}
|
||||
|
||||
// remove flag
|
||||
function removeFlag(str) {
|
||||
return str
|
||||
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]|🏴☠️|🏳️🌈/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function createDynamicFunction(name, script, $arguments) {
|
||||
const flowUtils = {
|
||||
getFlowField,
|
||||
|
||||
@@ -133,9 +133,13 @@ export default function Clash_Producer() {
|
||||
}
|
||||
}
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||
proxy.type,
|
||||
)
|
||||
[
|
||||
'trojan',
|
||||
'tuic',
|
||||
'hysteria',
|
||||
'hysteria2',
|
||||
'juicity',
|
||||
].includes(proxy.type)
|
||||
) {
|
||||
delete proxy.tls;
|
||||
}
|
||||
@@ -158,6 +162,7 @@ export default function Clash_Producer() {
|
||||
delete proxy.collectionName;
|
||||
delete proxy.id;
|
||||
delete proxy.resolved;
|
||||
delete proxy['no-resolve'];
|
||||
for (const key in proxy) {
|
||||
if (proxy[key] == null || /^_/i.test(key)) {
|
||||
delete proxy[key];
|
||||
|
||||
@@ -149,9 +149,13 @@ export default function ClashMeta_Producer() {
|
||||
}
|
||||
}
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||
proxy.type,
|
||||
)
|
||||
[
|
||||
'trojan',
|
||||
'tuic',
|
||||
'hysteria',
|
||||
'hysteria2',
|
||||
'juicity',
|
||||
].includes(proxy.type)
|
||||
) {
|
||||
delete proxy.tls;
|
||||
}
|
||||
@@ -173,6 +177,7 @@ export default function ClashMeta_Producer() {
|
||||
delete proxy.collectionName;
|
||||
delete proxy.id;
|
||||
delete proxy.resolved;
|
||||
delete proxy['no-resolve'];
|
||||
for (const key in proxy) {
|
||||
if (proxy[key] == null || /^_/i.test(key)) {
|
||||
delete proxy[key];
|
||||
|
||||
@@ -408,8 +408,8 @@ function wireguard(proxy) {
|
||||
}
|
||||
|
||||
function hysteria2(proxy) {
|
||||
if (proxy.obfs || proxy['obfs-password']) {
|
||||
throw new Error(`obfs is unsupported`);
|
||||
if (proxy['obfs-password'] && proxy.obfs != 'salamander') {
|
||||
throw new Error(`only salamander obfs is supported`);
|
||||
}
|
||||
const result = new Result(proxy);
|
||||
result.append(`${proxy.name}=Hysteria2,${proxy.server},${proxy.port}`);
|
||||
@@ -423,6 +423,10 @@ function hysteria2(proxy) {
|
||||
'skip-cert-verify',
|
||||
);
|
||||
|
||||
if (proxy['obfs-password'] && proxy.obfs == 'salamander') {
|
||||
result.append(`,salamander-password=${proxy['obfs-password']}`);
|
||||
}
|
||||
|
||||
// tfo
|
||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
|
||||
|
||||
@@ -152,9 +152,13 @@ export default function ShadowRocket_Producer() {
|
||||
}
|
||||
}
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||
proxy.type,
|
||||
)
|
||||
[
|
||||
'trojan',
|
||||
'tuic',
|
||||
'hysteria',
|
||||
'hysteria2',
|
||||
'juicity',
|
||||
].includes(proxy.type)
|
||||
) {
|
||||
delete proxy.tls;
|
||||
}
|
||||
@@ -176,6 +180,7 @@ export default function ShadowRocket_Producer() {
|
||||
delete proxy.collectionName;
|
||||
delete proxy.id;
|
||||
delete proxy.resolved;
|
||||
delete proxy['no-resolve'];
|
||||
for (const key in proxy) {
|
||||
if (proxy[key] == null || /^_/i.test(key)) {
|
||||
delete proxy[key];
|
||||
|
||||
@@ -202,8 +202,8 @@ const tlsParser = (proxy, parsedProxy) => {
|
||||
parsedProxy.tls.alpn = [proxy.alpn];
|
||||
} else if (Array.isArray(proxy.alpn)) parsedProxy.tls.alpn = proxy.alpn;
|
||||
if (proxy.ca) parsedProxy.tls.certificate_path = `${proxy.ca}`;
|
||||
if (proxy.ca_str) parsedProxy.tls.certificate = proxy.ca_sStr;
|
||||
if (proxy['ca-str']) parsedProxy.tls.certificate = proxy['ca-str'];
|
||||
if (proxy.ca_str) parsedProxy.tls.certificate = [proxy.ca_str];
|
||||
if (proxy['ca-str']) parsedProxy.tls.certificate = [proxy['ca-str']];
|
||||
if (proxy['client-fingerprint'] && proxy['client-fingerprint'] !== '')
|
||||
parsedProxy.tls.utls = {
|
||||
enabled: true,
|
||||
|
||||
@@ -21,6 +21,8 @@ export default function Stash_Producer() {
|
||||
'wireguard',
|
||||
'hysteria',
|
||||
'hysteria2',
|
||||
'ssh',
|
||||
'juicity',
|
||||
].includes(proxy.type) ||
|
||||
(proxy.type === 'ss' &&
|
||||
![
|
||||
@@ -232,9 +234,13 @@ export default function Stash_Producer() {
|
||||
}
|
||||
}
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||
proxy.type,
|
||||
)
|
||||
[
|
||||
'trojan',
|
||||
'tuic',
|
||||
'hysteria',
|
||||
'hysteria2',
|
||||
'juicity',
|
||||
].includes(proxy.type)
|
||||
) {
|
||||
delete proxy.tls;
|
||||
}
|
||||
@@ -265,6 +271,7 @@ export default function Stash_Producer() {
|
||||
delete proxy.collectionName;
|
||||
delete proxy.id;
|
||||
delete proxy.resolved;
|
||||
delete proxy['no-resolve'];
|
||||
for (const key in proxy) {
|
||||
if (proxy[key] == null || /^_/i.test(key)) {
|
||||
delete proxy[key];
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function Surge_Producer() {
|
||||
case 'trojan':
|
||||
return trojan(proxy);
|
||||
case 'vmess':
|
||||
return vmess(proxy);
|
||||
return vmess(proxy, opts['include-unsupported-proxy']);
|
||||
case 'http':
|
||||
return http(proxy);
|
||||
case 'socks5':
|
||||
@@ -264,7 +264,7 @@ function trojan(proxy) {
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function vmess(proxy) {
|
||||
function vmess(proxy, includeUnsupportedProxy) {
|
||||
const result = new Result(proxy);
|
||||
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||
result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
|
||||
@@ -278,7 +278,7 @@ function vmess(proxy) {
|
||||
);
|
||||
|
||||
// transport
|
||||
handleTransport(result, proxy);
|
||||
handleTransport(result, proxy, includeUnsupportedProxy);
|
||||
|
||||
// AEAD
|
||||
if (isPresent(proxy, 'aead')) {
|
||||
@@ -1013,7 +1013,7 @@ function hysteria2(proxy) {
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function handleTransport(result, proxy) {
|
||||
function handleTransport(result, proxy, includeUnsupportedProxy) {
|
||||
if (isPresent(proxy, 'network')) {
|
||||
if (proxy.network === 'ws') {
|
||||
result.append(`,ws=true`);
|
||||
@@ -1039,7 +1039,13 @@ function handleTransport(result, proxy) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error(`network ${proxy.network} is unsupported`);
|
||||
if (includeUnsupportedProxy && ['http'].includes(proxy.network)) {
|
||||
$.info(
|
||||
`Include Unsupported Proxy: nework ${proxy.network} -> tcp`,
|
||||
);
|
||||
} else {
|
||||
throw new Error(`network ${proxy.network} is unsupported`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,17 @@ export default function URI_Producer() {
|
||||
delete proxy.collectionName;
|
||||
delete proxy.id;
|
||||
delete proxy.resolved;
|
||||
delete proxy['no-resolve'];
|
||||
for (const key in proxy) {
|
||||
if (proxy[key] == null || /^_/i.test(key)) {
|
||||
delete proxy[key];
|
||||
}
|
||||
}
|
||||
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity'].includes(
|
||||
proxy.type,
|
||||
)
|
||||
) {
|
||||
delete proxy.tls;
|
||||
}
|
||||
if (proxy.server && isIPv6(proxy.server)) {
|
||||
|
||||
@@ -117,6 +117,7 @@ async function doSync() {
|
||||
await produceArtifact({
|
||||
type: 'subscription',
|
||||
name: subName,
|
||||
awaitCustomCache: true,
|
||||
});
|
||||
} catch (e) {
|
||||
// $.error(`${e.message ?? e}`);
|
||||
|
||||
@@ -123,10 +123,11 @@ async function downloadSubscription(req, res) {
|
||||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
||||
) {
|
||||
try {
|
||||
url = `${url || sub.url}`
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)?.[0];
|
||||
url =
|
||||
`${url || sub.url}`
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)?.[0] || '';
|
||||
|
||||
let $arguments = {};
|
||||
const rawArgs = url.split('#');
|
||||
@@ -156,6 +157,7 @@ async function downloadSubscription(req, res) {
|
||||
$arguments.flowUserAgent,
|
||||
undefined,
|
||||
sub.proxy,
|
||||
$arguments.flowUrl,
|
||||
);
|
||||
if (flowInfo) {
|
||||
res.set('subscription-userinfo', flowInfo);
|
||||
@@ -282,10 +284,11 @@ async function downloadCollection(req, res) {
|
||||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
||||
) {
|
||||
try {
|
||||
let url = `${sub.url}`
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)?.[0];
|
||||
let url =
|
||||
`${sub.url}`
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)?.[0] || '';
|
||||
|
||||
let $arguments = {};
|
||||
const rawArgs = url.split('#');
|
||||
@@ -314,6 +317,7 @@ async function downloadCollection(req, res) {
|
||||
$arguments.flowUserAgent,
|
||||
undefined,
|
||||
sub.proxy,
|
||||
$arguments.flowUrl,
|
||||
);
|
||||
if (flowInfo) {
|
||||
res.set('subscription-userinfo', flowInfo);
|
||||
|
||||
@@ -34,6 +34,11 @@ export default function register($app) {
|
||||
async function getFlowInfo(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
let { url } = req.query;
|
||||
if (url) {
|
||||
url = decodeURIComponent(url);
|
||||
$.info(`指定远程订阅 URL: ${url}`);
|
||||
}
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const sub = findByName(allSubs, name);
|
||||
if (!sub) {
|
||||
@@ -68,10 +73,11 @@ async function getFlowInfo(req, res) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let url = `${sub.url}`
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)?.[0];
|
||||
url =
|
||||
`${url || sub.url}`
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)?.[0] || '';
|
||||
|
||||
let $arguments = {};
|
||||
const rawArgs = url.split('#');
|
||||
@@ -118,6 +124,7 @@ async function getFlowInfo(req, res) {
|
||||
$arguments.flowUserAgent,
|
||||
undefined,
|
||||
sub.proxy,
|
||||
$arguments.flowUrl,
|
||||
);
|
||||
if (!flowHeaders) {
|
||||
failed(
|
||||
|
||||
@@ -36,6 +36,7 @@ async function produceArtifact({
|
||||
produceType,
|
||||
produceOpts = {},
|
||||
subscription,
|
||||
awaitCustomCache,
|
||||
}) {
|
||||
platform = platform || 'JSON';
|
||||
|
||||
@@ -67,6 +68,8 @@ async function produceArtifact({
|
||||
ua || sub.ua,
|
||||
undefined,
|
||||
sub.proxy,
|
||||
undefined,
|
||||
awaitCustomCache,
|
||||
);
|
||||
} catch (err) {
|
||||
errors[url] = err;
|
||||
@@ -112,6 +115,8 @@ async function produceArtifact({
|
||||
ua || sub.ua,
|
||||
undefined,
|
||||
sub.proxy,
|
||||
undefined,
|
||||
awaitCustomCache,
|
||||
);
|
||||
} catch (err) {
|
||||
errors[url] = err;
|
||||
@@ -503,6 +508,7 @@ async function syncArtifacts() {
|
||||
await produceArtifact({
|
||||
type: 'subscription',
|
||||
name: subName,
|
||||
awaitCustomCache: true,
|
||||
});
|
||||
} catch (e) {
|
||||
// $.error(`${e.message ?? e}`);
|
||||
|
||||
@@ -15,11 +15,12 @@ import $ from '@/core/app';
|
||||
const tasks = new Map();
|
||||
|
||||
export default async function download(
|
||||
rawUrl,
|
||||
rawUrl = '',
|
||||
ua,
|
||||
timeout,
|
||||
proxy,
|
||||
skipCustomCache,
|
||||
awaitCustomCache,
|
||||
) {
|
||||
let $arguments = {};
|
||||
let url = rawUrl.replace(/#noFlow$/, '');
|
||||
@@ -41,25 +42,66 @@ export default async function download(
|
||||
}
|
||||
}
|
||||
}
|
||||
const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();
|
||||
const { defaultUserAgent, defaultTimeout, cacheThreshold } =
|
||||
$.read(SETTINGS_KEY);
|
||||
const userAgent = ua || defaultUserAgent || 'clash.meta';
|
||||
const requestTimeout = timeout || defaultTimeout;
|
||||
const id = hex_md5(userAgent + url);
|
||||
|
||||
const customCacheKey = $arguments?.cacheKey
|
||||
? `#sub-store-cached-custom-${$arguments?.cacheKey}`
|
||||
: undefined;
|
||||
|
||||
if (customCacheKey && !skipCustomCache) {
|
||||
const cached = $.read(customCacheKey);
|
||||
if (cached) {
|
||||
const customCached = $.read(customCacheKey);
|
||||
const cached = resourceCache.get(id);
|
||||
if (!$arguments?.noCache && cached) {
|
||||
$.info(
|
||||
`乐观缓存: URL ${url}\n本次返回自定义缓存 ${$arguments?.cacheKey}\n并进行请求 尝试更新缓存`,
|
||||
);
|
||||
download(
|
||||
rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''),
|
||||
ua,
|
||||
timeout,
|
||||
proxy,
|
||||
true,
|
||||
`乐观缓存: URL ${url}\n存在有效的常规缓存\n使用常规缓存以避免重复请求`,
|
||||
);
|
||||
return cached;
|
||||
}
|
||||
if (customCached) {
|
||||
if (awaitCustomCache) {
|
||||
$.info(`乐观缓存: URL ${url}\n本次进行请求 尝试更新缓存`);
|
||||
try {
|
||||
await download(
|
||||
rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''),
|
||||
ua,
|
||||
timeout,
|
||||
proxy,
|
||||
true,
|
||||
);
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`乐观缓存: URL ${url} 更新缓存发生错误 ${
|
||||
e.message ?? e
|
||||
}`,
|
||||
);
|
||||
$.info('使用乐观缓存的数据刷新缓存, 防止后续请求');
|
||||
resourceCache.set(id, customCached);
|
||||
}
|
||||
} else {
|
||||
$.info(
|
||||
`乐观缓存: URL ${url}\n本次返回自定义缓存 ${$arguments?.cacheKey}\n并进行请求 尝试异步更新缓存`,
|
||||
);
|
||||
download(
|
||||
rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''),
|
||||
ua,
|
||||
timeout,
|
||||
proxy,
|
||||
true,
|
||||
).catch((e) => {
|
||||
$.error(
|
||||
`乐观缓存: URL ${url} 异步更新缓存发生错误 ${
|
||||
e.message ?? e
|
||||
}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
return customCached;
|
||||
}
|
||||
}
|
||||
|
||||
// const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/);
|
||||
@@ -79,12 +121,6 @@ export default async function download(
|
||||
// return item.content;
|
||||
// }
|
||||
|
||||
const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();
|
||||
const { defaultUserAgent, defaultTimeout, cacheThreshold } =
|
||||
$.read(SETTINGS_KEY);
|
||||
const userAgent = ua || defaultUserAgent || 'clash.meta';
|
||||
const requestTimeout = timeout || defaultTimeout;
|
||||
const id = hex_md5(userAgent + url);
|
||||
if (!isNode && tasks.has(id)) {
|
||||
return tasks.get(id);
|
||||
}
|
||||
@@ -104,9 +140,13 @@ export default async function download(
|
||||
|
||||
// try to find in app cache
|
||||
const cached = resourceCache.get(id);
|
||||
if (!$arguments?.noCache && cached && !skipCustomCache) {
|
||||
if (!$arguments?.noCache && cached) {
|
||||
$.info(`使用缓存: ${url}`);
|
||||
result = cached;
|
||||
if (customCacheKey) {
|
||||
$.info(`URL ${url}\n写入自定义缓存 ${$arguments?.cacheKey}`);
|
||||
$.write(cached, customCacheKey);
|
||||
}
|
||||
} else {
|
||||
$.info(
|
||||
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nURL: ${url}`,
|
||||
@@ -177,6 +217,7 @@ export default async function download(
|
||||
$arguments.flowUserAgent,
|
||||
undefined,
|
||||
proxy,
|
||||
$arguments.flowUrl,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -10,8 +10,8 @@ export function getFlowField(headers) {
|
||||
)[0];
|
||||
return headers[subkey];
|
||||
}
|
||||
export async function getFlowHeaders(rawUrl, ua, timeout, proxy) {
|
||||
let url = rawUrl;
|
||||
export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) {
|
||||
let url = flowUrl || rawUrl || '';
|
||||
let $arguments = {};
|
||||
const rawArgs = url.split('#');
|
||||
url = url.split('#')[0];
|
||||
@@ -48,60 +48,76 @@ export async function getFlowHeaders(rawUrl, ua, timeout, proxy) {
|
||||
'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)';
|
||||
const requestTimeout = timeout || defaultTimeout;
|
||||
const http = HTTP();
|
||||
try {
|
||||
if (flowUrl) {
|
||||
$.info(
|
||||
`使用 HEAD 方法获取流量信息: ${url}, User-Agent: ${
|
||||
`使用 GET 方法从响应体获取流量信息: ${flowUrl}, User-Agent: ${
|
||||
userAgent || ''
|
||||
}`,
|
||||
);
|
||||
const { headers } = await http.head({
|
||||
url: url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)[0],
|
||||
headers: {
|
||||
'User-Agent': userAgent,
|
||||
...(isStash && proxy
|
||||
? {
|
||||
'X-Stash-Selected-Proxy':
|
||||
encodeURIComponent(proxy),
|
||||
}
|
||||
: {}),
|
||||
...(isShadowRocket && proxy
|
||||
? { 'X-Surge-Policy': proxy }
|
||||
: {}),
|
||||
},
|
||||
timeout: requestTimeout,
|
||||
...(proxy ? { proxy } : {}),
|
||||
...(isLoon && proxy ? { node: proxy } : {}),
|
||||
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
|
||||
...(proxy ? getPolicyDescriptor(proxy) : {}),
|
||||
});
|
||||
flowInfo = getFlowField(headers);
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`使用 HEAD 方法获取流量信息失败: ${url}, User-Agent: ${
|
||||
userAgent || ''
|
||||
}: ${e.message ?? e}`,
|
||||
);
|
||||
}
|
||||
if (!flowInfo) {
|
||||
$.info(
|
||||
`使用 GET 方法获取流量信息: ${url}, User-Agent: ${
|
||||
userAgent || ''
|
||||
}`,
|
||||
);
|
||||
const { headers } = await http.get({
|
||||
url: url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)[0],
|
||||
const { body } = await http.get({
|
||||
url: flowUrl,
|
||||
headers: {
|
||||
'User-Agent': userAgent,
|
||||
},
|
||||
timeout: requestTimeout,
|
||||
});
|
||||
flowInfo = getFlowField(headers);
|
||||
flowInfo = body;
|
||||
} else {
|
||||
try {
|
||||
$.info(
|
||||
`使用 HEAD 方法从响应头获取流量信息: ${url}, User-Agent: ${
|
||||
userAgent || ''
|
||||
}`,
|
||||
);
|
||||
const { headers } = await http.head({
|
||||
url: url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)[0],
|
||||
headers: {
|
||||
'User-Agent': userAgent,
|
||||
...(isStash && proxy
|
||||
? {
|
||||
'X-Stash-Selected-Proxy':
|
||||
encodeURIComponent(proxy),
|
||||
}
|
||||
: {}),
|
||||
...(isShadowRocket && proxy
|
||||
? { 'X-Surge-Policy': proxy }
|
||||
: {}),
|
||||
},
|
||||
timeout: requestTimeout,
|
||||
...(proxy ? { proxy } : {}),
|
||||
...(isLoon && proxy ? { node: proxy } : {}),
|
||||
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
|
||||
...(proxy ? getPolicyDescriptor(proxy) : {}),
|
||||
});
|
||||
flowInfo = getFlowField(headers);
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`使用 HEAD 方法从响应头获取流量信息失败: ${url}, User-Agent: ${
|
||||
userAgent || ''
|
||||
}: ${e.message ?? e}`,
|
||||
);
|
||||
}
|
||||
if (!flowInfo) {
|
||||
$.info(
|
||||
`使用 GET 方法获取流量信息: ${url}, User-Agent: ${
|
||||
userAgent || ''
|
||||
}`,
|
||||
);
|
||||
const { headers } = await http.get({
|
||||
url: url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)[0],
|
||||
headers: {
|
||||
'User-Agent': userAgent,
|
||||
},
|
||||
timeout: requestTimeout,
|
||||
});
|
||||
flowInfo = getFlowField(headers);
|
||||
}
|
||||
}
|
||||
if (flowInfo) {
|
||||
headersResourceCache.set(url, flowInfo);
|
||||
|
||||
@@ -430,6 +430,13 @@ export function getISO(name) {
|
||||
return ISOFlags[getFlag(name)]?.[0];
|
||||
}
|
||||
|
||||
// remove flag
|
||||
export function removeFlag(str) {
|
||||
return str
|
||||
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]|🏴☠️|🏳️🌈/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export class MMDB {
|
||||
constructor({ country, asn } = {}) {
|
||||
if ($.env.isNode) {
|
||||
|
||||
11
backend/src/utils/rs.js
Normal file
11
backend/src/utils/rs.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import rs from 'jsrsasign';
|
||||
|
||||
export function generateFingerprint(caStr) {
|
||||
const hex = rs.pemtohex(caStr);
|
||||
const fingerPrint = rs.KJUR.crypto.Util.hashHex(hex, 'sha256');
|
||||
return fingerPrint.match(/.{2}/g).join(':').toUpperCase();
|
||||
}
|
||||
|
||||
export default {
|
||||
generateFingerprint,
|
||||
};
|
||||
6
backend/src/vendor/open-api.js
vendored
6
backend/src/vendor/open-api.js
vendored
@@ -382,6 +382,12 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
|
||||
url: options.url,
|
||||
headers: options.headers,
|
||||
body: options.body,
|
||||
options: {
|
||||
Proxy: options.proxy,
|
||||
Timeout: options.timeout
|
||||
? options.timeout / 1000
|
||||
: 15,
|
||||
},
|
||||
});
|
||||
resolve({
|
||||
statusCode: response.status,
|
||||
|
||||
@@ -6,13 +6,23 @@ Sub-Store Releases: [`https://github.com/sub-store-org/Sub-Store/releases`](http
|
||||
|
||||
Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
|
||||
|
||||
## 脚本配置:
|
||||
## 服务器/云平台/Docker/Android 版
|
||||
|
||||
https://xream.notion.site/Sub-Store-abe6a96944724dc6a36833d5c9ab7c87
|
||||
|
||||
## App 版
|
||||
|
||||
### 1. Loon
|
||||
安装使用 插件 [`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
|
||||
|
||||
#### 关于 Surge 的格外说明
|
||||
|
||||
Surge Mac 版如何支持 SSR, 如何去除 HTTP 传输层以支持 类似 VMess HTTP 节点等 请查看 [链接参数说明](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)
|
||||
|
||||
定时处理订阅 功能, 避免 App 内拉取超时, 请查看 [定时处理订阅](https://t.me/zhetengsha/1449)
|
||||
|
||||
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. 官方默认版模块(支持 App 内使用编辑参数): [`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)
|
||||
@@ -39,3 +49,11 @@ Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
|
||||
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。
|
||||
2. 可以把 Sub-Store 添加到主屏幕,即可获得类似于 APP 的使用体验。
|
||||
3. 更详细的使用指南请参考[文档](https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46)。
|
||||
|
||||
## 链接参数说明
|
||||
|
||||
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
|
||||
|
||||
## 脚本使用说明
|
||||
|
||||
https://github.com/sub-store-org/Sub-Store/wiki/%E8%84%9A%E6%9C%AC%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E
|
||||
@@ -7,12 +7,17 @@ function operator(proxies = [], targetPlatform, context) {
|
||||
// proxies 为传入的内部节点数组
|
||||
// 结构大致参考了 Clash.Meta(mihomo) 有私货
|
||||
// 可在预览界面点击节点查看 JSON 结构 或查看 `target=JSON` 的通用订阅
|
||||
// 1. `no-resolve` 为不解析域名
|
||||
// 2. 域名解析后 会多一个 `_resolved` 字段
|
||||
// 3. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_domain` 字段
|
||||
// 1. `_no-resolve` 为不解析域名
|
||||
// 2. 域名解析后 会多一个 `_resolved` 字段, 表示是否解析成功
|
||||
// 3. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_domain` 字段, `_resolved_ips` 为解析出的所有 IP
|
||||
// 4. 节点字段 `exec` 为 `ssr-local` 路径, 默认 `/usr/local/bin/ssr-local`; 端口从 10000 开始递增(暂不支持配置)
|
||||
// 5. `_subName` 为单条订阅名
|
||||
// 6. `_collectionName` 为组合订阅名
|
||||
// 7. `tls-fingerprint` 为 tls 指纹
|
||||
// 8. `underlying-proxy` 为前置代理
|
||||
// 9. `trojan`, `tuic`, `hysteria`, `hysteria2`, `juicity` 会在解析时设置 `tls`: true (会使用 tls 类协议的通用逻辑), 输出时删除
|
||||
// 10. `sni` 在某些协议里会自动与 `servername` 转换
|
||||
// 11. 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint (参考 https://t.me/zhetengsha/1512)
|
||||
|
||||
// $arguments 为传入的脚本参数
|
||||
|
||||
@@ -40,12 +45,15 @@ function operator(proxies = [], targetPlatform, context) {
|
||||
// isIP,
|
||||
// yaml, // yaml 解析和生成
|
||||
// getFlag, // 获取 emoji 旗帜
|
||||
// removeFlag, // 移除 emoji 旗帜
|
||||
// getISO, // 获取 ISO 3166-1 alpha-2 代码
|
||||
// Gist, // Gist 类
|
||||
// }
|
||||
|
||||
// 示例: 给节点名添加前缀
|
||||
// $server.name = `[${ProxyUtils.getISO($server.name)}] ${$server.name}`
|
||||
// 示例: 给节点名添加旗帜
|
||||
// $server.name = `[${ProxyUtils.getFlag($server.name).replace(/🇹🇼/g, '🇼🇸')}] ${ProxyUtils.removeFlag($server.name)}`
|
||||
|
||||
// 示例: 从 sni 文件中读取内容并进行节点操作
|
||||
// const sni = await produceArtifact({
|
||||
@@ -100,7 +108,7 @@ function operator(proxies = [], targetPlatform, context) {
|
||||
// 4. 一个比较折腾的方案: 在脚本操作中, 把内容同步到另一个 gist
|
||||
// 见 https://t.me/zhetengsha/1428
|
||||
//
|
||||
// const content = ProxyUtils.produce(proxies, platform)
|
||||
// const content = ProxyUtils.produce([...proxies], platform)
|
||||
|
||||
// // YAML
|
||||
// ProxyUtils.yaml.load('YAML String')
|
||||
|
||||
Reference in New Issue
Block a user