Compare commits

...

41 Commits

Author SHA1 Message Date
xream
c91d8e28e4 fix: 哪吒探针在线时长 2024-04-30 15:39:14 +08:00
xream
8cbb4492be feat: 全部是 WireGuard 节点的订阅, 支持输出为 Surge 模块 2024-04-25 16:55:32 +08:00
xream
6f7da57e3a fix: 旗帜操作中将 🏴‍☠️ 🏳️‍🌈 也视为已有旗帜并在删除后添加新旗帜 2024-04-25 13:38:01 +08:00
xream
2586c29746 fix: 处理手动删除 Gist 之后, Sub-Store 侧重新同步的逻辑 2024-04-23 09:28:39 +08:00
xream
6f7fe8204b feat: 支持完整导出和导入 Sub-Store 单条订阅数据 2024-04-22 15:15:57 +08:00
xream
bafaf07743 Merge pull request #314 from eric-gitta-moore/master
feat: script ip-flag for node
2024-04-22 13:09:33 +08:00
Eric Moore
9962eb0947 feat: script ip-flag for node 2024-04-22 12:59:06 +08:00
xream
ac5232a7bc ci: restore the functionality of generating conventional changelog 2024-04-22 04:25:19 +08:00
xream
2301ccbfb5 fix: 修复对不规范的节点名称的处理 2024-04-22 02:51:44 +08:00
xream
0b5761e5fc feat: QX 输出正式支持 VLESS 2024-04-22 02:15:54 +08:00
xream
3ab21b0e26 feat: 支持 Loon SOCKS5/SOCKS5-TLS 2024-04-21 12:36:11 +08:00
xream
89ab72e46c doc: README 2024-04-21 11:32:23 +08:00
xream
18bd6526d0 chore: 增加探针版本(没有自定义的必要吧 默认为 0.0.1) 2024-04-20 07:45:49 +08:00
xream
c7329c32eb feat: 哪吒探针网络监控接口支持用参数传入检测次数; 节点字段上自定义的多个次数, 只取最大值 2024-04-19 06:27:45 +08:00
xream
4819ae95e4 feat: 哪吒探针网络监控接口提示不兼容的节点, 支持传入节点名 2024-04-19 05:57:26 +08:00
xream
370d228b04 feat: 订阅兼容哪吒探针网络监控接口(Loon/Surge 可输出节点延迟) 2024-04-19 05:16:24 +08:00
xream
d092916168 fix: sing-box wireguard 2024-04-17 11:36:23 +08:00
xream
0c93de48ab feat: GEO 增加 TYO 2024-04-17 05:50:50 +08:00
xream
274aa50373 ci: GitHub Actions: Transitioning from Node 16 to Node 20 2024-04-17 01:12:07 +08:00
xream
e24de8d0b6 feat: 支持 WireGuard URI 输入和输出 2024-04-17 00:56:53 +08:00
xream
93a5ce6c3b feat: 支持 dialer-proxy, detour 2024-04-14 21:34:45 +08:00
xream
cb66c8daa2 feat: fancy-characters 增加 modifier-letter(小写没有 q, 用 ᵠ 替代. 大写缺的太多, 用小写替代) 2024-04-12 22:39:59 +08:00
xream
f4cdc953e6 feat: 支持设置并在远程订阅失败时读取最近一次成功的缓存 2024-04-09 20:49:42 +08:00
xream
2a1c2eb9df chore: 处理订阅输出哪吒探针兼容响应的 Uptime 字段 2024-04-09 13:48:33 +08:00
xream
6217c2e5cd fix: 修复 sing-box wireguard reserved 2024-04-07 19:48:08 +08:00
xream
f90d9c2fd1 chore: Surge 模块文案 2024-04-07 16:40:09 +08:00
xream
3e952e9e88 chore: Surge 默认模块更新为支持编辑参数的版本 2024-04-06 19:40:30 +08:00
xream
a81b55f752 doc: demo.js 2024-04-05 13:46:07 +08:00
xream
33652af516 feat: 订阅支持输出哪吒探针兼容响应; 清理输出数据; 增加内部数据字段 2024-04-05 13:37:15 +08:00
xream
2bca669930 fix: 修复 SS URI 解析错误 2024-04-04 16:52:46 +08:00
xream
f1bf0e1e8d fix: 修复 Tencent DNS 解析 2024-04-04 15:26:19 +08:00
xream
16b9cd9aaf feat: GEO 增加 AMS 2024-04-03 00:45:48 +08:00
xream
32eb069ab2 feat: GEO 增加 JNB, SJC, SEL 2024-04-02 21:06:56 +08:00
xream
4c9f8011c7 fix: 修复内蒙古识别为蒙古的问题 2024-04-02 20:56:07 +08:00
xream
bd26b0a561 feat: 区域过滤增加韩国德国 2024-04-02 20:40:13 +08:00
xream
958d1e52c8 chore: 日志输出增加订阅的来源 User-Agent 2024-03-31 10:59:49 +08:00
xream
e7a2e60963 feat: sing-box 订阅格式修改(如需原始格式 请使用 target=sing-box&produceType=internal); 清理 Clash 系无效字段 2024-03-31 09:36:32 +08:00
xream
fa6a274f79 feat: ProxyUtils 增加 getFlag, getISO 方法 2024-03-31 08:20:51 +08:00
xream
e40b3f88d5 chore: geo 增加关键词 德意志 2024-03-31 06:59:31 +08:00
xream
163ad9ee09 feat: JSON 输出支持 produceType internal 2024-03-31 04:45:57 +08:00
xream
abb6f2dec1 feat: 处理 sni off 的情况. 若出现问题, 麻烦大家及时反馈 2024-03-30 01:12:34 +08:00
35 changed files with 860 additions and 179 deletions

View File

@@ -21,11 +21,11 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: "16" node-version: "20"
- name: Install dependencies - name: Install dependencies
run: | run: |
npm install -g pnpm npm install -g pnpm
cd backend && pnpm i cd backend && pnpm i --no-frozen-lockfile
- name: Test - name: Test
run: | run: |
cd backend cd backend
@@ -44,14 +44,20 @@ jobs:
cd backend cd backend
SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"` SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"`
echo "release_tag=$SUBSTORE_RELEASE" >> $GITHUB_OUTPUT echo "release_tag=$SUBSTORE_RELEASE" >> $GITHUB_OUTPUT
- name: Prepare release
run: |
cd backend
pnpm i -D conventional-changelog-cli
pnpm run changelog
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
if: ${{ success() }} if: ${{ success() }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
body_path: ./backend/CHANGELOG.md
tag_name: ${{ steps.tag.outputs.release_tag }} tag_name: ${{ steps.tag.outputs.release_tag }}
generate_release_notes: true # generate_release_notes: true
files: | files: |
./backend/sub-store.min.js ./backend/sub-store.min.js
./backend/dist/sub-store-0.min.js ./backend/dist/sub-store-0.min.js

View File

@@ -26,11 +26,11 @@ Core functionalities:
### Supported Input Formats ### Supported Input Formats
- [x] URI(SS, SSR, VMess, VLESS, Trojan, Hysteria, Hysteria 2, TUIC v5) - [x] URI(SS, SSR, VMess, VLESS, Trojan, Hysteria, Hysteria 2, TUIC v5, WireGuard)
- [x] Clash Proxies YAML - [x] Clash Proxies YAML
- [x] Clash Proxy JSON(single line) - [x] Clash Proxy JSON(single line)
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS) - [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, WireGuard, VLESS, Hysteria 2) - [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, 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] 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] 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] Shadowrocket (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC)

View File

@@ -1,6 +1,6 @@
{ {
"name": "sub-store", "name": "sub-store",
"version": "2.14.266", "version": "2.14.303",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.", "description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js", "main": "src/main.js",
"scripts": { "scripts": {
@@ -11,7 +11,8 @@
"dev:esbuild": "nodemon -w src -w package.json dev-esbuild.js", "dev:esbuild": "nodemon -w src -w package.json dev-esbuild.js",
"dev:run": "nodemon -w sub-store.min.js sub-store.min.js", "dev:run": "nodemon -w sub-store.min.js sub-store.min.js",
"build": "gulp", "build": "gulp",
"bundle": "node bundle.js" "bundle": "node bundle.js",
"changelog": "conventional-changelog -p cli -i CHANGELOG.md -s"
}, },
"author": "Peng-YM", "author": "Peng-YM",
"license": "GPL-3.0", "license": "GPL-3.0",

View File

@@ -15,6 +15,7 @@ import $ from '@/core/app';
import { FILES_KEY, MODULES_KEY } from '@/constants'; import { FILES_KEY, MODULES_KEY } from '@/constants';
import { findByName } from '@/utils/database'; import { findByName } from '@/utils/database';
import { produceArtifact } from '@/restful/sync'; import { produceArtifact } from '@/restful/sync';
import { getFlag, getISO } from '@/utils/geo';
function preprocess(raw) { function preprocess(raw) {
for (const processor of PROXY_PREPROCESSORS) { for (const processor of PROXY_PREPROCESSORS) {
@@ -186,6 +187,10 @@ function produce(proxies, targetPlatform, type, opts = {}) {
throw new Error(`Target platform: ${targetPlatform} is not supported!`); throw new Error(`Target platform: ${targetPlatform} is not supported!`);
} }
const sni_off_supported = /Surge|SurgeMac|Shadowrocket/i.test(
targetPlatform,
);
// filter unsupported proxies // filter unsupported proxies
proxies = proxies.filter( proxies = proxies.filter(
(proxy) => (proxy) =>
@@ -196,13 +201,25 @@ function produce(proxies, targetPlatform, type, opts = {}) {
if (!isNotBlank(proxy.name)) { if (!isNotBlank(proxy.name)) {
proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`; proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;
} }
if (proxy['disable-sni']) {
if (sni_off_supported) {
proxy.sni = 'off';
} else if (!['tuic'].includes(proxy.type)) {
$.error(
`Target platform ${targetPlatform} does not support sni off. Proxy's fields (sni, tls-fingerprint and skip-cert-verify) will be modified.`,
);
proxy.sni = '';
proxy['skip-cert-verify'] = true;
delete proxy['tls-fingerprint'];
}
}
return proxy; return proxy;
}); });
$.log(`Producing proxies for target: ${targetPlatform}`); $.log(`Producing proxies for target: ${targetPlatform}`);
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') { if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
let localPort = 10000; let localPort = 10000;
const list = proxies let list = proxies
.map((proxy) => { .map((proxy) => {
try { try {
let line = producer.produce(proxy, type, opts); let line = producer.produce(proxy, type, opts);
@@ -228,7 +245,18 @@ function produce(proxies, targetPlatform, type, opts = {}) {
} }
}) })
.filter((line) => line.length > 0); .filter((line) => line.length > 0);
return type === 'internal' ? list : list.join('\n'); list = type === 'internal' ? list : list.join('\n');
if (
targetPlatform.startsWith('Surge') &&
proxies.length > 0 &&
proxies.every((p) => p.type === 'wireguard')
) {
list = `#!name=${proxies[0]?.subName}
#!desc=${proxies[0]?._desc ?? ''}
#!category=${proxies[0]?._category ?? ''}
${list}`;
}
return list;
} else if (producer.type === 'ALL') { } else if (producer.type === 'ALL') {
return producer.produce(proxies, type, opts); return producer.produce(proxies, type, opts);
} }
@@ -242,6 +270,8 @@ export const ProxyUtils = {
isIPv6, isIPv6,
isIP, isIP,
yaml: YAML, yaml: YAML,
getFlag,
getISO,
}; };
function tryParse(parser, line) { function tryParse(parser, line) {
@@ -385,17 +415,24 @@ function lastParse(proxy) {
} }
} }
if (typeof proxy.name !== 'string') { if (typeof proxy.name !== 'string') {
try { if (/^\d+$/.test(proxy.name)) {
if (proxy.name?.data) { proxy.name = `${proxy.name}`;
proxy.name = Buffer.from(proxy.name.data).toString('utf8'); } else {
} else { try {
proxy.name = utf8ArrayToStr(proxy.name); 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}`;
} }
} catch (e) {
$.error(`proxy.name decode failed\nReason: ${e}`);
proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;
} }
} }
if (['', 'off'].includes(proxy.sni)) {
proxy['disable-sni'] = true;
}
return proxy; return proxy;
} }

View File

@@ -63,9 +63,9 @@ function URI_SS() {
/\d+/, /\d+/,
)?.[0]; )?.[0];
const userInfo = userInfoStr.split(':'); const userInfo = userInfoStr.match(/(^.*?):(.*$)/);
proxy.cipher = userInfo[0]; proxy.cipher = userInfo[1];
proxy.password = userInfo[1]; proxy.password = userInfo[2];
// handle obfs // handle obfs
const idx = content.indexOf('?plugin='); const idx = content.indexOf('?plugin=');
@@ -674,6 +674,89 @@ function URI_TUIC() {
}; };
return { name, test, parse }; return { name, test, parse };
} }
function URI_WireGuard() {
const name = 'URI WireGuard Parser';
const test = (line) => {
return /^(wireguard|wg):\/\//.test(line);
};
const parse = (line) => {
line = line.split(/(wireguard|wg):\/\//)[2];
/* eslint-disable no-unused-vars */
let [
__,
___,
privateKey,
server,
____,
port,
_____,
addons = '',
name,
] = /^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
/* eslint-enable no-unused-vars */
port = parseInt(`${port}`, 10);
if (isNaN(port)) {
port = 51820;
}
privateKey = decodeURIComponent(privateKey);
if (name != null) {
name = decodeURIComponent(name);
}
name = name ?? `WireGuard ${server}:${port}`;
const proxy = {
type: 'wireguard',
name,
server,
port,
'private-key': privateKey,
udp: true,
};
for (const addon of addons.split('&')) {
let [key, value] = addon.split('=');
key = key.replace(/_/, '-');
value = decodeURIComponent(value);
if (['reserved'].includes(key)) {
const parsed = value
.split(',')
.map((i) => parseInt(i.trim(), 10))
.filter((i) => Number.isInteger(i));
if (parsed.length === 3) {
proxy[key] = parsed;
}
} else if (['address', 'ip'].includes(key)) {
value.split(',').map((i) => {
const ip = i
.trim()
.replace(/\/\d+$/, '')
.replace(/^\[/, '')
.replace(/\]$/, '');
if (isIPv4(ip)) {
proxy.ip = ip;
} else if (isIPv6(ip)) {
proxy.ipv6 = ip;
}
});
} else if (['mtu'].includes(key)) {
const parsed = parseInt(value.trim(), 10);
if (Number.isInteger(parsed)) {
proxy[key] = parsed;
}
} else if (/publickey/i.test(key)) {
proxy['public-key'] = value;
} else if (/privatekey/i.test(key)) {
proxy['private-key'] = value;
} else if (['udp'].includes(key)) {
proxy[key] = /(TRUE)|1/i.test(value);
} else if (!['flag'].includes(key)) {
proxy[key] = value;
}
}
return proxy;
};
return { name, test, parse };
}
// Trojan URI format // Trojan URI format
function URI_Trojan() { function URI_Trojan() {
@@ -751,6 +834,9 @@ function Clash_All() {
if (proxy.fingerprint) { if (proxy.fingerprint) {
proxy['tls-fingerprint'] = proxy.fingerprint; proxy['tls-fingerprint'] = proxy.fingerprint;
} }
if (proxy['dialer-proxy']) {
proxy['underlying-proxy'] = proxy['dialer-proxy'];
}
if (proxy['benchmark-url']) { if (proxy['benchmark-url']) {
proxy['test-url'] = proxy['benchmark-url']; proxy['test-url'] = proxy['benchmark-url'];
@@ -910,6 +996,15 @@ function Loon_Http() {
const parse = (line) => getLoonParser().parse(line); const parse = (line) => getLoonParser().parse(line);
return { name, test, parse }; return { name, test, parse };
} }
function Loon_Socks5() {
const name = 'Loon SOCKS5 Parser';
const test = (line) => {
return /^.*=\s*socks5/i.test(line.split(',')[0]);
};
const parse = (line) => getLoonParser().parse(line);
return { name, test, parse };
}
function Loon_WireGuard() { function Loon_WireGuard() {
const name = 'Loon WireGuard Parser'; const name = 'Loon WireGuard Parser';
@@ -1193,6 +1288,7 @@ export default [
URI_VMess(), URI_VMess(),
URI_VLESS(), URI_VLESS(),
URI_TUIC(), URI_TUIC(),
URI_WireGuard(),
URI_Hysteria(), URI_Hysteria(),
URI_Hysteria2(), URI_Hysteria2(),
URI_Trojan(), URI_Trojan(),
@@ -1215,6 +1311,7 @@ export default [
Loon_Hysteria2(), Loon_Hysteria2(),
Loon_Trojan(), Loon_Trojan(),
Loon_Http(), Loon_Http(),
Loon_Socks5(),
Loon_WireGuard(), Loon_WireGuard(),
QX_SS(), QX_SS(),
QX_SSR(), QX_SSR(),

View File

@@ -35,7 +35,7 @@ const grammars = String.raw`
} }
} }
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/hysteria2) { start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/socks5/hysteria2) {
return proxy; return proxy;
} }
@@ -78,6 +78,9 @@ https = tag equals "https"i address (username password)? (tls_host/tls_verificat
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* { http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* {
proxy.type = "http"; proxy.type = "http";
} }
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
proxy.type = "socks5";
}
address = comma server:server comma port:port { address = comma server:server comma port:port {
proxy.server = server; proxy.server = server;
@@ -167,7 +170,7 @@ ssr_protocol_param = comma "protocol-param" equals param:$[^=,]+ { proxy["protoc
vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); } vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); }
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; } over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
tls_host = comma "tls-name" equals host:domain { proxy.sni = host; } tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; }
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; } tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }

View File

@@ -33,7 +33,7 @@
} }
} }
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/hysteria2) { start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/socks5/hysteria2) {
return proxy; return proxy;
} }
@@ -76,6 +76,9 @@ https = tag equals "https"i address (username password)? (tls_host/tls_verificat
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* { http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* {
proxy.type = "http"; proxy.type = "http";
} }
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
proxy.type = "socks5";
}
address = comma server:server comma port:port { address = comma server:server comma port:port {
proxy.server = server; proxy.server = server;
@@ -165,7 +168,7 @@ ssr_protocol_param = comma "protocol-param" equals param:$[^=,]+ { proxy["protoc
vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); } vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); }
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; } over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
tls_host = comma "tls-name" equals host:domain { proxy.sni = host; } tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; }
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; } tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }

View File

@@ -177,7 +177,13 @@ username = & {
password = comma match:[^,]+ { proxy.password = match.join(""); } password = comma match:[^,]+ { proxy.password = match.join(""); }
tls = comma "tls" equals flag:bool { proxy.tls = flag; } tls = comma "tls" equals flag:bool { proxy.tls = flag; }
sni = comma "sni" equals sni:domain { proxy.sni = sni; } sni = comma "sni" equals sni:("off"/domain) {
if (sni === "off") {
proxy["disable-sni"] = true;
} else {
proxy.sni = sni;
}
}
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; } tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); } tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }

View File

@@ -175,7 +175,13 @@ username = & {
password = comma match:[^,]+ { proxy.password = match.join(""); } password = comma match:[^,]+ { proxy.password = match.join(""); }
tls = comma "tls" equals flag:bool { proxy.tls = flag; } tls = comma "tls" equals flag:bool { proxy.tls = flag; }
sni = comma "sni" equals sni:domain { proxy.sni = sni; } sni = comma "sni" equals sni:("off"/domain) {
if (sni === "off") {
proxy["disable-sni"] = true;
} else {
proxy.sni = sni;
}
}
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; } tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); } tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }

View File

@@ -22,6 +22,9 @@ function Base64Encoded() {
'aHR0c', // htt 'aHR0c', // htt
'dmxlc3M=', // vless 'dmxlc3M=', // vless
'aHlzdGVyaWEy', // hysteria2 'aHlzdGVyaWEy', // hysteria2
'd2lyZWd1YXJkOi8v', // wireguard://
'd2c6Ly8=', // wg://
'dHVpYzovLw==', // tuic://
]; ];
const test = function (raw) { const test = function (raw) {

View File

@@ -490,7 +490,7 @@ const DOMAIN_RESOLVERS = {
}, },
}); });
const answers = resp.body.split(';').map((i) => i.split(',')[0]); const answers = resp.body.split(';').map((i) => i.split(',')[0]);
if (answers.length === 0) { if (answers.length === 0 || String(answers) === '0') {
throw new Error('No answers'); throw new Error('No answers');
} }
const result = answers[answers.length - 1]; const result = answers[answers.length - 1];
@@ -550,13 +550,25 @@ function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
results[p.server], results[p.server],
); );
if (server && port) { if (server && port) {
p._domain = p.server;
p.server = server; p.server = server;
p.port = port; p.port = port;
p.resolved = true; p.resolved = true;
p._IPv4 = p.server;
if (!isIP(p._IP)) {
p._IP = p.server;
}
} else {
p.resolved = false;
} }
} else { } else {
p._domain = p.server;
p.server = results[p.server]; p.server = results[p.server];
p.resolved = true; p.resolved = true;
p[`_${type}`] = p.server;
if (!isIP(p._IP)) {
p._IP = p.server;
}
} }
} else { } else {
p.resolved = false; p.resolved = false;
@@ -617,6 +629,8 @@ function RegionFilter(regions) {
SG: '🇸🇬', SG: '🇸🇬',
JP: '🇯🇵', JP: '🇯🇵',
UK: '🇬🇧', UK: '🇬🇧',
DE: '🇩🇪',
KR: '🇰🇷',
}; };
return { return {
name: 'Region Filter', name: 'Region Filter',
@@ -853,7 +867,7 @@ function clone(object) {
// remove flag // remove flag
function removeFlag(str) { function removeFlag(str) {
return str return str
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, '') .replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]|🏴‍☠️|🏳️‍🌈/g, '')
.trim(); .trim();
} }

View File

@@ -144,12 +144,25 @@ export default function Clash_Producer() {
proxy.fingerprint = proxy['tls-fingerprint']; proxy.fingerprint = proxy['tls-fingerprint'];
} }
delete proxy['tls-fingerprint']; delete proxy['tls-fingerprint'];
if (proxy['underlying-proxy']) {
proxy['dialer-proxy'] = proxy['underlying-proxy'];
}
delete proxy['underlying-proxy'];
if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') { if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
delete proxy.tls; delete proxy.tls;
} }
delete proxy.subName; delete proxy.subName;
delete proxy.collectionName; delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
}
if ( if (
['grpc'].includes(proxy.network) && ['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`] proxy[`${proxy.network}-opts`]

View File

@@ -160,11 +160,24 @@ export default function ClashMeta_Producer() {
proxy.fingerprint = proxy['tls-fingerprint']; proxy.fingerprint = proxy['tls-fingerprint'];
} }
delete proxy['tls-fingerprint']; delete proxy['tls-fingerprint'];
if (proxy['underlying-proxy']) {
proxy['dialer-proxy'] = proxy['underlying-proxy'];
}
delete proxy['underlying-proxy'];
if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') { if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
delete proxy.tls; delete proxy.tls;
} }
delete proxy.subName; delete proxy.subName;
delete proxy.collectionName; delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
}
if ( if (
['grpc'].includes(proxy.network) && ['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`] proxy[`${proxy.network}-opts`]

View File

@@ -13,7 +13,8 @@ import singbox_Producer from './sing-box';
function JSON_Producer() { function JSON_Producer() {
const type = 'ALL'; const type = 'ALL';
const produce = (proxies) => JSON.stringify(proxies, null, 2); const produce = (proxies, type) =>
type === 'internal' ? proxies : JSON.stringify(proxies, null, 2);
return { type, produce }; return { type, produce };
} }

View File

@@ -18,6 +18,8 @@ export default function Loon_Producer() {
return vless(proxy); return vless(proxy);
case 'http': case 'http':
return http(proxy); return http(proxy);
case 'socks5':
return socks5(proxy);
case 'wireguard': case 'wireguard':
return wireguard(proxy); return wireguard(proxy);
case 'hysteria2': case 'hysteria2':
@@ -316,6 +318,29 @@ function http(proxy) {
return result.toString(); return result.toString();
} }
function socks5(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=socks5,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,"${proxy.password}"`, 'password');
// tls
result.appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
// sni
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
// tls verification
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
// tfo
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
return result.toString();
}
function wireguard(proxy) { function wireguard(proxy) {
if (Array.isArray(proxy.peers) && proxy.peers.length > 0) { if (Array.isArray(proxy.peers) && proxy.peers.length > 0) {

View File

@@ -3,6 +3,7 @@ import { isPresent, Result } from './utils';
const targetPlatform = 'QX'; const targetPlatform = 'QX';
export default function QX_Producer() { export default function QX_Producer() {
// eslint-disable-next-line no-unused-vars
const produce = (proxy, type, opts = {}) => { const produce = (proxy, type, opts = {}) => {
switch (proxy.type) { switch (proxy.type) {
case 'ss': case 'ss':
@@ -18,13 +19,7 @@ export default function QX_Producer() {
case 'socks5': case 'socks5':
return socks5(proxy); return socks5(proxy);
case 'vless': case 'vless':
if (opts['include-unsupported-proxy']) { return vless(proxy);
return vless(proxy);
} else {
throw new Error(
`Platform ${targetPlatform}(App Store Release) does not support proxy type: ${proxy.type}`,
);
}
} }
throw new Error( throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`, `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,

View File

@@ -163,11 +163,24 @@ export default function ShadowRocket_Producer() {
proxy.fingerprint = proxy['tls-fingerprint']; proxy.fingerprint = proxy['tls-fingerprint'];
} }
delete proxy['tls-fingerprint']; delete proxy['tls-fingerprint'];
if (proxy['underlying-proxy']) {
proxy['dialer-proxy'] = proxy['underlying-proxy'];
}
delete proxy['underlying-proxy'];
if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') { if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
delete proxy.tls; delete proxy.tls;
} }
delete proxy.subName; delete proxy.subName;
delete proxy.collectionName; delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
}
if ( if (
['grpc'].includes(proxy.network) && ['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`] proxy[`${proxy.network}-opts`]

View File

@@ -1,6 +1,10 @@
import ClashMeta_Producer from './clashmeta'; import ClashMeta_Producer from './clashmeta';
import $ from '@/core/app'; import $ from '@/core/app';
import { isIPv4, isIPv6 } from '@/utils';
const detourParser = (proxy, parsedProxy) => {
if (proxy['dialer-proxy']) parsedProxy.detour = proxy['dialer-proxy'];
};
const tfoParser = (proxy, parsedProxy) => { const tfoParser = (proxy, parsedProxy) => {
parsedProxy.tcp_fast_open = false; parsedProxy.tcp_fast_open = false;
if (proxy.tfo) parsedProxy.tcp_fast_open = true; if (proxy.tfo) parsedProxy.tcp_fast_open = true;
@@ -249,6 +253,7 @@ const sshParser = (proxy = {}) => {
parsedProxy.host_key_algorithms = proxy['host-key-algorithms']; parsedProxy.host_key_algorithms = proxy['host-key-algorithms'];
if (proxy['fast-open']) parsedProxy.udp_fragment = true; if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
return parsedProxy; return parsedProxy;
}; };
@@ -274,6 +279,7 @@ const httpParser = (proxy = {}) => {
} }
if (proxy['fast-open']) parsedProxy.udp_fragment = true; if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy);
return parsedProxy; return parsedProxy;
}; };
@@ -295,6 +301,7 @@ const socks5Parser = (proxy = {}) => {
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true; if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
if (proxy['fast-open']) parsedProxy.udp_fragment = true; if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
return parsedProxy; return parsedProxy;
}; };
@@ -326,6 +333,7 @@ const shadowTLSParser = (proxy = {}) => {
throw '端口值非法'; throw '端口值非法';
if (proxy['fast-open'] === true) stPart.udp_fragment = true; if (proxy['fast-open'] === true) stPart.udp_fragment = true;
tfoParser(proxy, stPart); tfoParser(proxy, stPart);
detourParser(proxy, stPart);
smuxParser(proxy.smux, ssPart); smuxParser(proxy.smux, ssPart);
return { type: 'ss-with-st', ssPart, stPart }; return { type: 'ss-with-st', ssPart, stPart };
}; };
@@ -344,6 +352,7 @@ const ssParser = (proxy = {}) => {
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true; if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
if (proxy['fast-open']) parsedProxy.udp_fragment = true; if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy); smuxParser(proxy.smux, parsedProxy);
if (proxy.plugin) { if (proxy.plugin) {
const optArr = []; const optArr = [];
@@ -421,6 +430,7 @@ const ssrParser = (proxy = {}) => {
parsedProxy.protocol_param = proxy['protocol-param']; parsedProxy.protocol_param = proxy['protocol-param'];
if (proxy['fast-open']) parsedProxy.udp_fragment = true; if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy); smuxParser(proxy.smux, parsedProxy);
return parsedProxy; return parsedProxy;
}; };
@@ -457,6 +467,7 @@ const vmessParser = (proxy = {}) => {
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy); if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy); smuxParser(proxy.smux, parsedProxy);
return parsedProxy; return parsedProxy;
@@ -479,6 +490,7 @@ const vlessParser = (proxy = {}) => {
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy); if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy); smuxParser(proxy.smux, parsedProxy);
tlsParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy);
return parsedProxy; return parsedProxy;
@@ -499,6 +511,7 @@ const trojanParser = (proxy = {}) => {
if (proxy.network === 'ws') wsParser(proxy, parsedProxy); if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy); smuxParser(proxy.smux, parsedProxy);
return parsedProxy; return parsedProxy;
@@ -545,6 +558,7 @@ const hysteriaParser = (proxy = {}) => {
} }
} }
tlsParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy); smuxParser(proxy.smux, parsedProxy);
return parsedProxy; return parsedProxy;
@@ -569,6 +583,7 @@ const hysteria2Parser = (proxy = {}) => {
if (!parsedProxy.obfs.type) delete parsedProxy.obfs; if (!parsedProxy.obfs.type) delete parsedProxy.obfs;
tlsParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy); smuxParser(proxy.smux, parsedProxy);
return parsedProxy; return parsedProxy;
}; };
@@ -597,6 +612,7 @@ const tuic5Parser = (proxy = {}) => {
if (proxy['heartbeat-interval']) if (proxy['heartbeat-interval'])
parsedProxy.heartbeat = `${proxy['heartbeat-interval']}ms`; parsedProxy.heartbeat = `${proxy['heartbeat-interval']}ms`;
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy); smuxParser(proxy.smux, parsedProxy);
return parsedProxy; return parsedProxy;
@@ -605,8 +621,11 @@ const tuic5Parser = (proxy = {}) => {
const wireguardParser = (proxy = {}) => { const wireguardParser = (proxy = {}) => {
const local_address = ['ip', 'ipv6'] const local_address = ['ip', 'ipv6']
.map((i) => proxy[i]) .map((i) => proxy[i])
.filter((i) => i) .map((i) => {
.map((i) => (/\\/.test(i) ? i : `${i}/32`)); if (isIPv4(i)) return `${i}/32`;
if (isIPv6(i)) return `${i}/128`;
})
.filter((i) => i);
const parsedProxy = { const parsedProxy = {
tag: proxy.name, tag: proxy.name,
type: 'wireguard', type: 'wireguard',
@@ -622,7 +641,7 @@ const wireguardParser = (proxy = {}) => {
throw 'invalid port'; throw 'invalid port';
if (proxy['fast-open']) parsedProxy.udp_fragment = true; if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (typeof proxy.reserved === 'string') { if (typeof proxy.reserved === 'string') {
parsedProxy.reserved.push(proxy.reserved); parsedProxy.reserved = proxy.reserved;
} else if (Array.isArray(proxy.reserved)) { } else if (Array.isArray(proxy.reserved)) {
for (const r of proxy.reserved) parsedProxy.reserved.push(r); for (const r of proxy.reserved) parsedProxy.reserved.push(r);
} else { } else {
@@ -650,6 +669,7 @@ const wireguardParser = (proxy = {}) => {
} }
} }
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy); smuxParser(proxy.smux, parsedProxy);
return parsedProxy; return parsedProxy;
}; };
@@ -789,7 +809,10 @@ export default function singbox_Producer() {
$.error(e.message ?? e); $.error(e.message ?? e);
} }
}); });
return type === 'internal' ? list : JSON.stringify(list, null, 2);
return type === 'internal'
? list
: JSON.stringify({ outbounds: list }, null, 2);
}; };
return { type, produce }; return { type, produce };
} }

View File

@@ -242,6 +242,12 @@ export default function Stash_Producer() {
proxy.fingerprint = proxy['tls-fingerprint']; proxy.fingerprint = proxy['tls-fingerprint'];
} }
delete proxy['tls-fingerprint']; delete proxy['tls-fingerprint'];
if (proxy['underlying-proxy']) {
proxy['dialer-proxy'] = proxy['underlying-proxy'];
}
delete proxy['underlying-proxy'];
if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') { if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
delete proxy.tls; delete proxy.tls;
} }
@@ -257,6 +263,13 @@ export default function Stash_Producer() {
delete proxy.subName; delete proxy.subName;
delete proxy.collectionName; delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
}
if ( if (
['grpc'].includes(proxy.network) && ['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`] proxy[`${proxy.network}-opts`]

View File

@@ -762,8 +762,8 @@ function wireguard(proxy) {
} }
const result = new Result(proxy); const result = new Result(proxy);
result.append(`# WireGuard Proxy ${proxy.name} result.append(`# > WireGuard Proxy ${proxy.name}
${proxy.name}=wireguard`); # ${proxy.name}=wireguard`);
proxy['section-name'] = getIfNotBlank(proxy['section-name'], proxy.name); proxy['section-name'] = getIfNotBlank(proxy['section-name'], proxy.name);
@@ -821,7 +821,7 @@ ${proxy.name}=wireguard`);
); );
result.append(` result.append(`
# WireGuard Section ${proxy.name} # > WireGuard Section ${proxy.name}
[WireGuard ${proxy['section-name']}] [WireGuard ${proxy['section-name']}]
private-key = ${proxy['private-key']}`); private-key = ${proxy['private-key']}`);

View File

@@ -8,6 +8,13 @@ export default function URI_Producer() {
let result = ''; let result = '';
delete proxy.subName; delete proxy.subName;
delete proxy.collectionName; delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
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'].includes(proxy.type)) {
delete proxy.tls; delete proxy.tls;
} }
@@ -404,8 +411,51 @@ export default function URI_Producer() {
}?${tuicParams.join('&')}#${encodeURIComponent( }?${tuicParams.join('&')}#${encodeURIComponent(
proxy.name, proxy.name,
)}`; )}`;
break;
} }
break;
case 'wireguard':
let wireguardParams = [];
Object.keys(proxy).forEach((key) => {
if (
![
'name',
'type',
'server',
'port',
'ip',
'ipv6',
'private-key',
].includes(key)
) {
if (['public-key'].includes(key)) {
wireguardParams.push(`publickey=${proxy[key]}`);
} else if (['udp'].includes(key)) {
if (proxy[key]) {
wireguardParams.push(`${key}=1`);
}
} else if (proxy[key]) {
wireguardParams.push(
`${key}=${encodeURIComponent(proxy[key])}`,
);
}
}
});
if (proxy.ip && proxy.ipv6) {
wireguardParams.push(
`address=${proxy.ip}/32,${proxy.ipv6}/128`,
);
} else if (proxy.ip) {
wireguardParams.push(`address=${proxy.ip}/32`);
} else if (proxy.ipv6) {
wireguardParams.push(`address=${proxy.ipv6}/128`);
}
result = `wireguard://${encodeURIComponent(
proxy['private-key'],
)}@${proxy.server}:${proxy.port}/?${wireguardParams.join(
'&',
)}#${encodeURIComponent(proxy.name)}`;
break;
} }
return result; return result;
}; };

View File

@@ -253,7 +253,25 @@ async function syncToGist(files) {
key: ARTIFACT_REPOSITORY_KEY, key: ARTIFACT_REPOSITORY_KEY,
syncPlatform, syncPlatform,
}); });
return manager.upload(files); const res = await manager.upload(files);
let body = {};
try {
body = JSON.parse(res.body);
// eslint-disable-next-line no-empty
} catch (e) {}
const url = body?.html_url ?? body?.web_url;
const settings = $.read(SETTINGS_KEY);
if (url) {
$.log(`同步 Gist 后, 找到 Sub-Store Gist: ${url}`);
settings.artifactStore = url;
settings.artifactStoreStatus = 'VALID';
} else {
$.error(`同步 Gist 后, 找不到 Sub-Store Gist`);
settings.artifactStoreStatus = 'NOT FOUND';
}
$.write(settings, SETTINGS_KEY);
return res;
} }
export { syncToGist }; export { syncToGist };

View File

@@ -1,4 +1,5 @@
import { getPlatformFromHeaders } from '@/utils/platform'; import { getPlatformFromHeaders } from '@/utils/user-agent';
import { ProxyUtils } from '@/core/proxy-utils';
import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants'; import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
import { findByName } from '@/utils/database'; import { findByName } from '@/utils/database';
import { getFlowHeaders } from '@/utils/flow'; import { getFlowHeaders } from '@/utils/flow';
@@ -6,15 +7,50 @@ import $ from '@/core/app';
import { failed } from '@/restful/response'; import { failed } from '@/restful/response';
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors'; import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
import { produceArtifact } from '@/restful/sync'; import { produceArtifact } from '@/restful/sync';
// eslint-disable-next-line no-unused-vars
import { isIPv4, isIPv6 } from '@/utils';
import { getISO } from '@/utils/geo';
import env from '@/utils/env';
export default function register($app) { export default function register($app) {
$app.get('/download/collection/:name', downloadCollection); $app.get('/download/collection/:name', downloadCollection);
$app.get('/download/:name', downloadSubscription); $app.get('/download/:name', downloadSubscription);
$app.get(
'/download/collection/:name/api/v1/server/details',
async (req, res) => {
req.query.platform = 'JSON';
req.query.produceType = 'internal';
req.query.resultFormat = 'nezha';
await downloadCollection(req, res);
},
);
$app.get('/download/:name/api/v1/server/details', async (req, res) => {
req.query.platform = 'JSON';
req.query.produceType = 'internal';
req.query.resultFormat = 'nezha';
await downloadSubscription(req, res);
});
$app.get(
'/download/collection/:name/api/v1/monitor/:nezhaIndex',
async (req, res) => {
req.query.platform = 'JSON';
req.query.produceType = 'internal';
req.query.resultFormat = 'nezha-monitor';
await downloadCollection(req, res);
},
);
$app.get('/download/:name/api/v1/monitor/:nezhaIndex', async (req, res) => {
req.query.platform = 'JSON';
req.query.produceType = 'internal';
req.query.resultFormat = 'nezha-monitor';
await downloadSubscription(req, res);
});
} }
async function downloadSubscription(req, res) { async function downloadSubscription(req, res) {
let { name } = req.params; let { name, nezhaIndex } = req.params;
name = decodeURIComponent(name); name = decodeURIComponent(name);
nezhaIndex = decodeURIComponent(nezhaIndex);
const platform = const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON'; req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
@@ -28,6 +64,7 @@ async function downloadSubscription(req, res) {
ignoreFailedRemoteSub, ignoreFailedRemoteSub,
produceType, produceType,
includeUnsupportedProxy, includeUnsupportedProxy,
resultFormat,
} = req.query; } = req.query;
if (url) { if (url) {
url = decodeURIComponent(url); url = decodeURIComponent(url);
@@ -62,7 +99,7 @@ async function downloadSubscription(req, res) {
const sub = findByName(allSubs, name); const sub = findByName(allSubs, name);
if (sub) { if (sub) {
try { try {
const output = await produceArtifact({ let output = await produceArtifact({
type: 'subscription', type: 'subscription',
name, name,
platform, platform,
@@ -133,6 +170,18 @@ async function downloadSubscription(req, res) {
} }
if (platform === 'JSON') { if (platform === 'JSON') {
if (resultFormat === 'nezha') {
output = nezhaTransform(output);
} else if (resultFormat === 'nezha-monitor') {
nezhaIndex = /^\d+$/.test(nezhaIndex)
? parseInt(nezhaIndex, 10)
: output.findIndex((i) => i.name === nezhaIndex);
output = await nezhaMonitor(
output[nezhaIndex],
nezhaIndex,
req.query,
);
}
res.set('Content-Type', 'application/json;charset=utf-8').send( res.set('Content-Type', 'application/json;charset=utf-8').send(
output, output,
); );
@@ -169,8 +218,9 @@ async function downloadSubscription(req, res) {
} }
async function downloadCollection(req, res) { async function downloadCollection(req, res) {
let { name } = req.params; let { name, nezhaIndex } = req.params;
name = decodeURIComponent(name); name = decodeURIComponent(name);
nezhaIndex = decodeURIComponent(nezhaIndex);
const platform = const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON'; req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
@@ -180,8 +230,12 @@ async function downloadCollection(req, res) {
$.info(`正在下载组合订阅:${name}`); $.info(`正在下载组合订阅:${name}`);
let { ignoreFailedRemoteSub, produceType, includeUnsupportedProxy } = let {
req.query; ignoreFailedRemoteSub,
produceType,
includeUnsupportedProxy,
resultFormat,
} = req.query;
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') { if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub); ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
@@ -199,7 +253,7 @@ async function downloadCollection(req, res) {
if (collection) { if (collection) {
try { try {
const output = await produceArtifact({ let output = await produceArtifact({
type: 'collection', type: 'collection',
name, name,
platform, platform,
@@ -271,6 +325,18 @@ async function downloadCollection(req, res) {
} }
if (platform === 'JSON') { if (platform === 'JSON') {
if (resultFormat === 'nezha') {
output = nezhaTransform(output);
} else if (resultFormat === 'nezha-monitor') {
nezhaIndex = /^\d+$/.test(nezhaIndex)
? parseInt(nezhaIndex, 10)
: output.findIndex((i) => i.name === nezhaIndex);
output = await nezhaMonitor(
output[nezhaIndex],
nezhaIndex,
req.query,
);
}
res.set('Content-Type', 'application/json;charset=utf-8').send( res.set('Content-Type', 'application/json;charset=utf-8').send(
output, output,
); );
@@ -307,3 +373,149 @@ async function downloadCollection(req, res) {
); );
} }
} }
async function nezhaMonitor(proxy, index, query) {
const result = {
code: 0,
message: 'success',
result: [],
};
try {
const { isLoon, isSurge } = $.env;
if (!isLoon && !isSurge)
throw new Error('仅支持 Loon 和 Surge(ability=http-client-policy)');
const node = ProxyUtils.produce([proxy], isLoon ? 'Loon' : 'Surge');
if (!node) throw new Error('当前客户端不兼容此节点');
const monitors = proxy._monitors || [
{
name: 'Cloudflare',
url: 'http://cp.cloudflare.com/generate_204',
method: 'HEAD',
number: 3,
timeout: 2000,
},
{
name: 'Google',
url: 'http://www.google.com/generate_204',
method: 'HEAD',
number: 3,
timeout: 2000,
},
];
const number =
query.number || Math.max(...monitors.map((i) => i.number)) || 3;
for (const monitor of monitors) {
const interval = 10 * 60 * 1000;
const data = {
monitor_id: monitors.indexOf(monitor),
server_id: index,
monitor_name: monitor.name,
server_name: proxy.name,
created_at: [],
avg_delay: [],
};
for (let index = 0; index < number; index++) {
const startedAt = Date.now();
try {
await $.http[(monitor.method || 'HEAD').toLowerCase()]({
timeout: monitor.timeout || 2000,
url: monitor.url,
'policy-descriptor': node,
node,
});
const latency = Date.now() - startedAt;
$.info(`${monitor.name} latency: ${latency}`);
data.avg_delay.push(latency);
} catch (e) {
$.error(e);
data.avg_delay.push(0);
}
data.created_at.push(
Date.now() - interval * (monitor.number - index - 1),
);
}
result.result.push(data);
}
} catch (e) {
$.error(e);
result.result.push({
monitor_id: 0,
server_id: 0,
monitor_name: `${e.message ?? e}`,
server_name: proxy.name,
created_at: [Date.now()],
avg_delay: [0],
});
}
return JSON.stringify(result, null, 2);
}
function nezhaTransform(output) {
const result = {
code: 0,
message: 'success',
result: [],
};
output.map((proxy, index) => {
// 如果节点上有数据 就取节点上的数据
let CountryCode = proxy._geo?.countryCode || proxy._geo?.country;
// 简单判断下
if (!/^[a-z]{2}$/i.test(CountryCode)) {
CountryCode = getISO(proxy.name);
}
// 简单判断下
if (/^[a-z]{2}$/i.test(CountryCode)) {
// 如果节点上有数据 就取节点上的数据
let now = Math.round(new Date().getTime() / 1000);
let time = proxy._unavailable ? 0 : now;
const uptime = parseInt(proxy._uptime || 0, 10);
result.result.push({
id: index,
name: proxy.name,
tag: `${proxy._tag ?? ''}`,
last_active: time,
// 暂时不用处理 现在 VPings App 端的接口支持域名查询
// 其他场景使用 自己在 Sub-Store 加一步域名解析
valid_ip: proxy._IP || proxy.server,
ipv4: proxy._IPv4 || proxy.server,
ipv6: proxy._IPv6 || (isIPv6(proxy.server) ? proxy.server : ''),
host: {
Platform: 'Sub-Store',
PlatformVersion: env.version,
CPU: [],
MemTotal: 1024,
DiskTotal: 1024,
SwapTotal: 1024,
Arch: '',
Virtualization: '',
BootTime: now - uptime,
CountryCode, // 目前需要
Version: '0.0.1',
},
status: {
CPU: 0,
MemUsed: 0,
SwapUsed: 0,
DiskUsed: 0,
NetInTransfer: 0,
NetOutTransfer: 0,
NetInSpeed: 0,
NetOutSpeed: 0,
Uptime: uptime,
Load1: 0,
Load5: 0,
Load15: 0,
TcpConnCount: 0,
UdpConnCount: 0,
ProcessCount: 0,
},
});
}
});
return JSON.stringify(result, null, 2);
}

View File

@@ -181,11 +181,21 @@ function createSubscription(req, res) {
function getSubscription(req, res) { function getSubscription(req, res) {
let { name } = req.params; let { name } = req.params;
let { raw } = req.query;
name = decodeURIComponent(name); name = decodeURIComponent(name);
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name); const sub = findByName(allSubs, name);
if (sub) { if (sub) {
success(res, sub); if (raw) {
res.set('content-type', 'application/json')
.set(
'content-disposition',
`attachment; filename="${encodeURIComponent(name)}.json"`,
)
.send(JSON.stringify(sub));
} else {
success(res, sub);
}
} else { } else {
failed( failed(
res, res,

View File

@@ -35,6 +35,9 @@ export default async function download(rawUrl, ua, timeout, proxy) {
} }
} }
} }
const customCacheKey = $arguments?.cacheKey
? `#sub-store-cached-custom-${$arguments?.cacheKey}`
: undefined;
// const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/); // const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/);
// if (downloadUrlMatch) { // if (downloadUrlMatch) {
@@ -116,10 +119,24 @@ export default async function download(rawUrl, ua, timeout, proxy) {
} }
if (shouldCache) { if (shouldCache) {
resourceCache.set(id, body); resourceCache.set(id, body);
if (customCacheKey) {
$.write(body, customCacheKey);
}
} }
result = body; result = body;
} catch (e) { } catch (e) {
if (customCacheKey) {
const cached = $.read(customCacheKey);
if (cached) {
$.info(
`无法下载 URL ${url}: ${
e.message ?? e
}\n使用自定义缓存 ${$arguments?.cacheKey}`,
);
return cached;
}
}
throw new Error(`无法下载 URL ${url}: ${e.message ?? e}`); throw new Error(`无法下载 URL ${url}: ${e.message ?? e}`);
} }
} }

View File

@@ -1,3 +1,105 @@
const ISOFlags = {
'🏳️‍🌈': ['EXP', 'BAND'],
'🇸🇱': ['TEST', 'SOS'],
'🇦🇩': ['AD', 'AND'],
'🇦🇪': ['AE', 'ARE'],
'🇦🇫': ['AF', 'AFG'],
'🇦🇱': ['AL', 'ALB'],
'🇦🇲': ['AM', 'ARM'],
'🇦🇷': ['AR', 'ARG'],
'🇦🇹': ['AT', 'AUT'],
'🇦🇺': ['AU', 'AUS'],
'🇦🇿': ['AZ', 'AZE'],
'🇧🇦': ['BA', 'BIH'],
'🇧🇩': ['BD', 'BGD'],
'🇧🇪': ['BE', 'BEL'],
'🇧🇬': ['BG', 'BGR'],
'🇧🇭': ['BH', 'BHR'],
'🇧🇷': ['BR', 'BRA'],
'🇧🇾': ['BY', 'BLR'],
'🇨🇦': ['CA', 'CAN'],
'🇨🇭': ['CH', 'CHE'],
'🇨🇱': ['CL', 'CHL'],
'🇨🇴': ['CO', 'COL'],
'🇨🇷': ['CR', 'CRI'],
'🇨🇾': ['CY', 'CYP'],
'🇨🇿': ['CZ', 'CZE'],
'🇩🇪': ['DE', 'DEU'],
'🇩🇰': ['DK', 'DNK'],
'🇪🇨': ['EC', 'ECU'],
'🇪🇪': ['EE', 'EST'],
'🇪🇬': ['EG', 'EGY'],
'🇪🇸': ['ES', 'ESP'],
'🇪🇺': ['EU'],
'🇫🇮': ['FI', 'FIN'],
'🇫🇷': ['FR', 'FRA'],
'🇬🇧': ['GB', 'GBR', 'UK'],
'🇬🇪': ['GE', 'GEO'],
'🇬🇷': ['GR', 'GRC'],
'🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'],
'🇭🇷': ['HR', 'HRV'],
'🇭🇺': ['HU', 'HUN'],
'🇯🇴': ['JO', 'JOR'],
'🇯🇵': ['JP', 'JPN', 'TYO'],
'🇰🇪': ['KE', 'KEN'],
'🇰🇬': ['KG', 'KGZ'],
'🇰🇭': ['KH', 'KGZ'],
'🇰🇵': ['KP', 'PRK'],
'🇰🇷': ['KR', 'KOR', 'SEL'],
'🇰🇿': ['KZ', 'KAZ'],
'🇮🇩': ['ID', 'IDN'],
'🇮🇪': ['IE', 'IRL'],
'🇮🇱': ['IL', 'ISR'],
'🇮🇲': ['IM', 'IMN'],
'🇮🇳': ['IN', 'IND'],
'🇮🇷': ['IR', 'IRN'],
'🇮🇸': ['IS', 'ISL'],
'🇮🇹': ['IT', 'ITA'],
'🇱🇹': ['LT', 'LTU'],
'🇱🇺': ['LU', 'LUX'],
'🇱🇻': ['LV', 'LVA'],
'🇲🇦': ['MA', 'MAR'],
'🇲🇩': ['MD', 'MDA'],
'🇳🇬': ['NG', 'NGA'],
'🇲🇰': ['MK', 'MKD'],
'🇲🇳': ['MN', 'MNG'],
'🇲🇴': ['MO', 'MAC', 'CTM'],
'🇲🇹': ['MT', 'MLT'],
'🇲🇽': ['MX', 'MEX'],
'🇲🇾': ['MY', 'MYS'],
'🇳🇱': ['NL', 'NLD', 'AMS'],
'🇳🇴': ['NO', 'NOR'],
'🇳🇵': ['NP', 'NPL'],
'🇳🇿': ['NZ', 'NZL'],
'🇵🇦': ['PA', 'PAN'],
'🇵🇪': ['PE', 'PER'],
'🇵🇭': ['PH', 'PHL'],
'🇵🇰': ['PK', 'PAK'],
'🇵🇱': ['PL', 'POL'],
'🇵🇷': ['PR', 'PRI'],
'🇵🇹': ['PT', 'PRT'],
'🇵🇾': ['PY', 'PRY'],
'🇷🇴': ['RO', 'ROU'],
'🇷🇸': ['RS', 'SRB'],
'🇷🇪': ['RE', 'REU'],
'🇷🇺': ['RU', 'RUS'],
'🇸🇦': ['SA', 'SAU'],
'🇸🇪': ['SE', 'SWE'],
'🇸🇬': ['SG', 'SGP'],
'🇸🇮': ['SI', 'SVN'],
'🇸🇰': ['SK', 'SVK'],
'🇹🇭': ['TH', 'THA'],
'🇹🇳': ['TN', 'TUN'],
'🇹🇷': ['TR', 'TUR'],
'🇹🇼': ['TW', 'TWN', 'CHT', 'HINET', 'ROC'],
'🇺🇦': ['UA', 'UKR'],
'🇺🇸': ['US', 'USA', 'LAX', 'SFO', 'SJC'],
'🇺🇾': ['UY', 'URY'],
'🇻🇪': ['VE', 'VEN'],
'🇻🇳': ['VN', 'VNM'],
'🇿🇦': ['ZA', 'ZAF', 'JNB'],
'🇨🇳': ['CN', 'CHN', 'BACK'],
};
// get proxy flag according to its name // get proxy flag according to its name
export function getFlag(name) { export function getFlag(name) {
// flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js // flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js
@@ -65,6 +167,7 @@ export function getFlag(name) {
'广德', '广德',
'法兰克福', '法兰克福',
'Frankfurt', 'Frankfurt',
'德意志',
], ],
'🇩🇰': ['Denmark', '丹麦', '丹麥'], '🇩🇰': ['Denmark', '丹麦', '丹麥'],
'🇪🇨': ['Ecuador', '厄瓜多尔'], '🇪🇨': ['Ecuador', '厄瓜多尔'],
@@ -283,108 +386,6 @@ export function getFlag(name) {
], ],
}; };
const ISOFlags = {
'🏳️‍🌈': ['EXP', 'BAND'],
'🇸🇱': ['TEST', 'SOS'],
'🇦🇩': ['AD', 'AND'],
'🇦🇪': ['AE', 'ARE'],
'🇦🇫': ['AF', 'AFG'],
'🇦🇱': ['AL', 'ALB'],
'🇦🇲': ['AM', 'ARM'],
'🇦🇷': ['AR', 'ARG'],
'🇦🇹': ['AT', 'AUT'],
'🇦🇺': ['AU', 'AUS'],
'🇦🇿': ['AZ', 'AZE'],
'🇧🇦': ['BA', 'BIH'],
'🇧🇩': ['BD', 'BGD'],
'🇧🇪': ['BE', 'BEL'],
'🇧🇬': ['BG', 'BGR'],
'🇧🇭': ['BH', 'BHR'],
'🇧🇷': ['BR', 'BRA'],
'🇧🇾': ['BY', 'BLR'],
'🇨🇦': ['CA', 'CAN'],
'🇨🇭': ['CH', 'CHE'],
'🇨🇱': ['CL', 'CHL'],
'🇨🇴': ['CO', 'COL'],
'🇨🇷': ['CR', 'CRI'],
'🇨🇾': ['CY', 'CYP'],
'🇨🇿': ['CZ', 'CZE'],
'🇩🇪': ['DE', 'DEU'],
'🇩🇰': ['DK', 'DNK'],
'🇪🇨': ['EC', 'ECU'],
'🇪🇪': ['EE', 'EST'],
'🇪🇬': ['EG', 'EGY'],
'🇪🇸': ['ES', 'ESP'],
'🇪🇺': ['EU'],
'🇫🇮': ['FI', 'FIN'],
'🇫🇷': ['FR', 'FRA'],
'🇬🇧': ['GB', 'GBR', 'UK'],
'🇬🇪': ['GE', 'GEO'],
'🇬🇷': ['GR', 'GRC'],
'🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'],
'🇭🇷': ['HR', 'HRV'],
'🇭🇺': ['HU', 'HUN'],
'🇯🇴': ['JO', 'JOR'],
'🇯🇵': ['JP', 'JPN'],
'🇰🇪': ['KE', 'KEN'],
'🇰🇬': ['KG', 'KGZ'],
'🇰🇭': ['KH', 'KGZ'],
'🇰🇵': ['KP', 'PRK'],
'🇰🇷': ['KR', 'KOR'],
'🇰🇿': ['KZ', 'KAZ'],
'🇮🇩': ['ID', 'IDN'],
'🇮🇪': ['IE', 'IRL'],
'🇮🇱': ['IL', 'ISR'],
'🇮🇲': ['IM', 'IMN'],
'🇮🇳': ['IN', 'IND'],
'🇮🇷': ['IR', 'IRN'],
'🇮🇸': ['IS', 'ISL'],
'🇮🇹': ['IT', 'ITA'],
'🇱🇹': ['LT', 'LTU'],
'🇱🇺': ['LU', 'LUX'],
'🇱🇻': ['LV', 'LVA'],
'🇲🇦': ['MA', 'MAR'],
'🇲🇩': ['MD', 'MDA'],
'🇳🇬': ['NG', 'NGA'],
'🇲🇰': ['MK', 'MKD'],
'🇲🇳': ['MN', 'MNG'],
'🇲🇴': ['MO', 'MAC', 'CTM'],
'🇲🇹': ['MT', 'MLT'],
'🇲🇽': ['MX', 'MEX'],
'🇲🇾': ['MY', 'MYS'],
'🇳🇱': ['NL', 'NLD'],
'🇳🇴': ['NO', 'NOR'],
'🇳🇵': ['NP', 'NPL'],
'🇳🇿': ['NZ', 'NZL'],
'🇵🇦': ['PA', 'PAN'],
'🇵🇪': ['PE', 'PER'],
'🇵🇭': ['PH', 'PHL'],
'🇵🇰': ['PK', 'PAK'],
'🇵🇱': ['PL', 'POL'],
'🇵🇷': ['PR', 'PRI'],
'🇵🇹': ['PT', 'PRT'],
'🇵🇾': ['PY', 'PRY'],
'🇷🇴': ['RO', 'ROU'],
'🇷🇸': ['RS', 'SRB'],
'🇷🇪': ['RE', 'REU'],
'🇷🇺': ['RU', 'RUS'],
'🇸🇦': ['SA', 'SAU'],
'🇸🇪': ['SE', 'SWE'],
'🇸🇬': ['SG', 'SGP'],
'🇸🇮': ['SI', 'SVN'],
'🇸🇰': ['SK', 'SVK'],
'🇹🇭': ['TH', 'THA'],
'🇹🇳': ['TN', 'TUN'],
'🇹🇷': ['TR', 'TUR'],
'🇹🇼': ['TW', 'TWN', 'CHT', 'HINET', 'ROC'],
'🇺🇦': ['UA', 'UKR'],
'🇺🇸': ['US', 'USA', 'LAX', 'SFO'],
'🇺🇾': ['UY', 'URY'],
'🇻🇪': ['VE', 'VEN'],
'🇻🇳': ['VN', 'VNM'],
'🇿🇦': ['ZA', 'ZAF'],
'🇨🇳': ['CN', 'CHN', 'BACK'],
};
// 原旗帜或空 // 原旗帜或空
let Flag = let Flag =
name.match(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/)?.[0] || name.match(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/)?.[0] ||
@@ -398,7 +399,9 @@ export function getFlag(name) {
// 不精确匹配(只要包含就算,忽略大小写) // 不精确匹配(只要包含就算,忽略大小写)
keywords.some((keyword) => RegExp(`${keyword}`, 'i').test(name)) keywords.some((keyword) => RegExp(`${keyword}`, 'i').test(name))
) { ) {
//console.log(`newFlag = ${flag}`) if (/内蒙古/.test(name) && ['🇲🇳'].includes(flag)) {
return (Flag = '🇨🇳');
}
return (Flag = flag); return (Flag = flag);
} }
} }
@@ -416,6 +419,11 @@ export function getFlag(name) {
return (Flag = flag); return (Flag = flag);
} }
} }
//console.log(`Final Flag = ${Flag}`) //console.log(`Final Flag = ${Flag}`)
return Flag; return Flag;
} }
export function getISO(name) {
return ISOFlags[getFlag(name)]?.[0];
}

View File

@@ -135,9 +135,9 @@ export default class Gist {
} }
} }
}); });
console.log(`result`, result); // console.log(`result`, result);
console.log(`files`, files); // console.log(`files`, files);
console.log(`actions`, actions); // console.log(`actions`, actions);
if (this.syncPlatform === 'gitlab') { if (this.syncPlatform === 'gitlab') {
if (Object.keys(result).length === 0) { if (Object.keys(result).length === 0) {

View File

@@ -1,4 +1,4 @@
export function getPlatformFromHeaders(headers) { export function getUserAgentFromHeaders(headers) {
const keys = Object.keys(headers); const keys = Object.keys(headers);
let UA = ''; let UA = '';
let ua = ''; let ua = '';
@@ -9,6 +9,9 @@ export function getPlatformFromHeaders(headers) {
break; break;
} }
} }
return { UA, ua };
}
export function getPlatformFromUserAgent({ ua, UA }) {
if (UA.indexOf('Quantumult%20X') !== -1) { if (UA.indexOf('Quantumult%20X') !== -1) {
return 'QX'; return 'QX';
} else if (UA.indexOf('Surfboard') !== -1) { } else if (UA.indexOf('Surfboard') !== -1) {
@@ -38,3 +41,7 @@ export function getPlatformFromHeaders(headers) {
return 'JSON'; return 'JSON';
} }
} }
export function getPlatformFromHeaders(headers) {
const { UA, ua } = getUserAgentFromHeaders(headers);
return getPlatformFromUserAgent({ ua, UA });
}

View File

@@ -13,15 +13,13 @@ Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
### 2. Surge ### 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) 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) 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)
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 参数版本, 使用 jsc 引擎时, 可能会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者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)
3. 固定不带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule)
### 3. QX ### 3. QX

View File

@@ -1,15 +1,15 @@
#!name=Sub-Store(β) #!name=Sub-Store(β)
#!desc=支持最新 Surge iOS TestFlight 版本的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * * #!desc=支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *
#!category=订阅管理 #!category=订阅管理
#!arguments=ability:http-client-policy,cronexp:55 23 * * *,sync:"Sub-Store Sync" #!arguments=ability:http-client-policy,cronexp:55 23 * * *,sync:"Sub-Store Sync",timeout:120,engine:auto
#!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若设为 # 可取消定时任务" #!arguments-desc=\n1⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n3⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4⃣ timeout\n\n超时, 单位为秒\n\n5⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存
[MITM] [MITM]
hostname = %APPEND% sub.store hostname = %APPEND% sub.store
[Script] [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 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={{{timeout}}},ability="{{{ability}}}",engine={{{engine}}}
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,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,timeout={{{timeout}}},engine={{{engine}}}
{{{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 {{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}}

View File

@@ -1,5 +1,5 @@
#!name=Sub-Store #!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 可能会爆内存, 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用不带 ability 参数版本. 定时任务默认为每天 23 点 55 分 #!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 使用 jsc 引擎时, 可能会爆内存, 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用不带 ability 参数版本. 定时任务默认为每天 23 点 55 分
#!category=订阅管理 #!category=订阅管理
[MITM] [MITM]

View File

@@ -1,12 +1,15 @@
#!name=Sub-Store #!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 23 点 55 分 #!desc=支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *
#!category=订阅管理 #!category=订阅管理
#!arguments=ability:http-client-policy,cronexp:55 23 * * *,sync:"Sub-Store Sync",timeout:120,engine:auto
#!arguments-desc=\n1⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n3⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4⃣ timeout\n\n超时, 单位为秒\n\n5⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存
[MITM] [MITM]
hostname = %APPEND% sub.store hostname = %APPEND% sub.store
[Script] [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 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={{{timeout}}},ability="{{{ability}}}",engine={{{engine}}}
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,timeout=120
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 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,timeout={{{timeout}}},engine={{{engine}}}
{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}}

View File

@@ -9,7 +9,8 @@ function operator(proxies = [], targetPlatform, context) {
// 可在预览界面点击节点查看 JSON 结构 或查看 `target=JSON` 的通用订阅 // 可在预览界面点击节点查看 JSON 结构 或查看 `target=JSON` 的通用订阅
// 1. `no-resolve` 为不解析域名 // 1. `no-resolve` 为不解析域名
// 2. 域名解析后 会多一个 `resolved` 字段 // 2. 域名解析后 会多一个 `resolved` 字段
// 3. 节点字段 `exec` 为 `ssr-local` 路径, 默认 `/usr/local/bin/ssr-local`; 端口从 10000 开始递增(暂不支持配置) // 3. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_domain` 字段
// 4. 节点字段 `exec` 为 `ssr-local` 路径, 默认 `/usr/local/bin/ssr-local`; 端口从 10000 开始递增(暂不支持配置)
// $arguments 为传入的脚本参数 // $arguments 为传入的脚本参数
@@ -36,8 +37,13 @@ function operator(proxies = [], targetPlatform, context) {
// isIPv6, // isIPv6,
// isIP, // isIP,
// yaml, // yaml 解析和生成 // yaml, // yaml 解析和生成
// getFlag, // 获取 emoji 旗帜
// getISO, // 获取 ISO 3166-1 alpha-2 代码
// } // }
// 示例: 给节点名添加前缀
// $server.name = `[${ProxyUtils.getISO($server.name)}] ${$server.name}`
// 示例: 从 sni 文件中读取内容并进行节点操作 // 示例: 从 sni 文件中读取内容并进行节点操作
// const sni = await produceArtifact({ // const sni = await produceArtifact({
// type: 'file', // type: 'file',

View File

@@ -3,7 +3,7 @@
* *
* 【字体】 * 【字体】
* 可参考https://www.dute.org/weird-fonts * 可参考https://www.dute.org/weird-fonts
* serif-bold, serif-italic, serif-bold-italic, sans-serif-regular, sans-serif-bold-italic, script-regular, script-bold, fraktur-regular, fraktur-bold, monospace-regular, double-struck-bold, circle-regular, square-regular * serif-bold, serif-italic, serif-bold-italic, sans-serif-regular, sans-serif-bold-italic, script-regular, script-bold, fraktur-regular, fraktur-bold, monospace-regular, double-struck-bold, circle-regular, square-regular, modifier-letter(小写没有 q, 用 ᵠ 替代. 大写缺的太多, 用小写替代)
* *
* 【示例】 * 【示例】
* 1⃣ 设置所有格式为 "serif-bold" * 1⃣ 设置所有格式为 "serif-bold"
@@ -31,6 +31,7 @@ function operator(proxies) {
"double-struck-bold": ["𝟘","𝟙","𝟚","𝟛","𝟜","𝟝","𝟞","𝟟","𝟠","𝟡","𝕒","𝕓","𝕔","𝕕","𝕖","𝕗","𝕘","𝕙","𝕚","𝕛","𝕜","𝕝","𝕞","𝕟","𝕠","𝕡","𝕢","𝕣","𝕤","𝕥","𝕦","𝕧","𝕨","𝕩","𝕪","𝕫","𝔸","𝔹","","𝔻","𝔼","𝔽","𝔾","","𝕀","𝕁","𝕂","𝕃","𝕄","","𝕆","","","","𝕊","𝕋","𝕌","𝕍","𝕎","𝕏","𝕐",""], "double-struck-bold": ["𝟘","𝟙","𝟚","𝟛","𝟜","𝟝","𝟞","𝟟","𝟠","𝟡","𝕒","𝕓","𝕔","𝕕","𝕖","𝕗","𝕘","𝕙","𝕚","𝕛","𝕜","𝕝","𝕞","𝕟","𝕠","𝕡","𝕢","𝕣","𝕤","𝕥","𝕦","𝕧","𝕨","𝕩","𝕪","𝕫","𝔸","𝔹","","𝔻","𝔼","𝔽","𝔾","","𝕀","𝕁","𝕂","𝕃","𝕄","","𝕆","","","","𝕊","𝕋","𝕌","𝕍","𝕎","𝕏","𝕐",""],
"circle-regular": ["⓪","①","②","③","④","⑤","⑥","⑦","⑧","⑨","ⓐ","ⓑ","ⓒ","ⓓ","ⓔ","ⓕ","ⓖ","ⓗ","ⓘ","ⓙ","ⓚ","ⓛ","ⓜ","ⓝ","ⓞ","ⓟ","ⓠ","ⓡ","ⓢ","ⓣ","ⓤ","ⓥ","ⓦ","ⓧ","ⓨ","ⓩ","Ⓐ","Ⓑ","Ⓒ","Ⓓ","Ⓔ","Ⓕ","Ⓖ","Ⓗ","Ⓘ","Ⓙ","Ⓚ","Ⓛ","Ⓜ","Ⓝ","Ⓞ","Ⓟ","Ⓠ","Ⓡ","Ⓢ","Ⓣ","Ⓤ","Ⓥ","Ⓦ","Ⓧ","Ⓨ","Ⓩ"], "circle-regular": ["⓪","①","②","③","④","⑤","⑥","⑦","⑧","⑨","ⓐ","ⓑ","ⓒ","ⓓ","ⓔ","ⓕ","ⓖ","ⓗ","ⓘ","ⓙ","ⓚ","ⓛ","ⓜ","ⓝ","ⓞ","ⓟ","ⓠ","ⓡ","ⓢ","ⓣ","ⓤ","ⓥ","ⓦ","ⓧ","ⓨ","ⓩ","Ⓐ","Ⓑ","Ⓒ","Ⓓ","Ⓔ","Ⓕ","Ⓖ","Ⓗ","Ⓘ","Ⓙ","Ⓚ","Ⓛ","Ⓜ","Ⓝ","Ⓞ","Ⓟ","Ⓠ","Ⓡ","Ⓢ","Ⓣ","Ⓤ","Ⓥ","Ⓦ","Ⓧ","Ⓨ","Ⓩ"],
"square-regular": ["0","1","2","3","4","5","6","7","8","9","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉"], "square-regular": ["0","1","2","3","4","5","6","7","8","9","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉"],
"modifier-letter": ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹", "ᵃ", "ᵇ", "ᶜ", "ᵈ", "ᵉ", "ᶠ", "ᵍ", "ʰ", "ⁱ", "ʲ", "ᵏ", "ˡ", "ᵐ", "ⁿ", "ᵒ", "ᵖ", "ᵠ", "ʳ", "ˢ", "ᵗ", "ᵘ", "ᵛ", "ʷ", "ˣ", "ʸ", "ᶻ", "ᴬ", "ᴮ", "ᶜ", "ᴰ", "ᴱ", "ᶠ", "ᴳ", "ʰ", "ᴵ", "ᴶ", "ᴷ", "ᴸ", "ᴹ", "ᴺ", "ᴼ", "ᴾ", "ᵠ", "ᴿ", "ˢ", "ᵀ", "ᵁ", "ᵛ", "ᵂ", "ˣ", "ʸ", "ᶻ"],
}; };
// charCode => index in `TABLE` // charCode => index in `TABLE`

79
scripts/ip-flag-node.js Normal file
View File

@@ -0,0 +1,79 @@
const $ = $substore;
const {onlyFlagIP = true} = $arguments
async function operator(proxies) {
const BATCH_SIZE = 10;
let i = 0;
while (i < proxies.length) {
const batch = proxies.slice(i, i + BATCH_SIZE);
await Promise.all(batch.map(async proxy => {
if (onlyFlagIP && !ProxyUtils.isIP(proxy.server)) return;
try {
// remove the original flag
let proxyName = removeFlag(proxy.name);
// query ip-api
const countryCode = await queryIpApi(proxy);
proxyName = getFlagEmoji(countryCode) + ' ' + proxyName;
proxy.name = proxyName;
} catch (err) {
// TODO:
}
}));
await sleep(1000);
i += BATCH_SIZE;
}
return proxies;
}
async function queryIpApi(proxy) {
const ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:78.0) Gecko/20100101 Firefox/78.0";
const headers = {
"User-Agent": ua
};
const result = new Promise((resolve, reject) => {
const url =
`http://ip-api.com/json/${encodeURIComponent(proxy.server)}?lang=zh-CN`;
$.http.get({
url,
headers,
}).then(resp => {
const data = JSON.parse(resp.body);
if (data.status === "success") {
resolve(data.countryCode);
} else {
reject(new Error(data.message));
}
}).catch(err => {
console.log(err);
reject(err);
});
});
return result;
}
function getFlagEmoji(countryCode) {
const codePoints = countryCode
.toUpperCase()
.split('')
.map(char => 127397 + char.charCodeAt());
return String
.fromCodePoint(...codePoints)
.replace(/🇹🇼/g, '🇨🇳');
}
function removeFlag(str) {
return str
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, '')
.trim();
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}