mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f7fe8204b | ||
|
|
bafaf07743 | ||
|
|
9962eb0947 | ||
|
|
ac5232a7bc | ||
|
|
2301ccbfb5 | ||
|
|
0b5761e5fc | ||
|
|
3ab21b0e26 | ||
|
|
89ab72e46c | ||
|
|
18bd6526d0 | ||
|
|
c7329c32eb | ||
|
|
4819ae95e4 | ||
|
|
370d228b04 | ||
|
|
d092916168 | ||
|
|
0c93de48ab | ||
|
|
274aa50373 | ||
|
|
e24de8d0b6 |
12
.github/workflows/main.yml
vendored
12
.github/workflows/main.yml
vendored
@@ -21,11 +21,11 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "16"
|
||||
node-version: "20"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm install -g pnpm
|
||||
cd backend && pnpm i
|
||||
cd backend && pnpm i --no-frozen-lockfile
|
||||
- name: Test
|
||||
run: |
|
||||
cd backend
|
||||
@@ -44,14 +44,20 @@ jobs:
|
||||
cd backend
|
||||
SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"`
|
||||
echo "release_tag=$SUBSTORE_RELEASE" >> $GITHUB_OUTPUT
|
||||
- name: Prepare release
|
||||
run: |
|
||||
cd backend
|
||||
pnpm i -D conventional-changelog-cli
|
||||
pnpm run changelog
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{ success() }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
body_path: ./backend/CHANGELOG.md
|
||||
tag_name: ${{ steps.tag.outputs.release_tag }}
|
||||
generate_release_notes: true
|
||||
# generate_release_notes: true
|
||||
files: |
|
||||
./backend/sub-store.min.js
|
||||
./backend/dist/sub-store-0.min.js
|
||||
|
||||
@@ -26,11 +26,11 @@ Core functionalities:
|
||||
|
||||
### 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 Proxy JSON(single line)
|
||||
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS)
|
||||
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, WireGuard, VLESS, Hysteria 2)
|
||||
- [x] 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] 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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sub-store",
|
||||
"version": "2.14.282",
|
||||
"version": "2.14.298",
|
||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
@@ -11,7 +11,8 @@
|
||||
"dev:esbuild": "nodemon -w src -w package.json dev-esbuild.js",
|
||||
"dev:run": "nodemon -w sub-store.min.js sub-store.min.js",
|
||||
"build": "gulp",
|
||||
"bundle": "node bundle.js"
|
||||
"bundle": "node bundle.js",
|
||||
"changelog": "conventional-changelog -p cli -i CHANGELOG.md -s"
|
||||
},
|
||||
"author": "Peng-YM",
|
||||
"license": "GPL-3.0",
|
||||
|
||||
@@ -404,15 +404,19 @@ function lastParse(proxy) {
|
||||
}
|
||||
}
|
||||
if (typeof proxy.name !== 'string') {
|
||||
try {
|
||||
if (proxy.name?.data) {
|
||||
proxy.name = Buffer.from(proxy.name.data).toString('utf8');
|
||||
} else {
|
||||
proxy.name = utf8ArrayToStr(proxy.name);
|
||||
if (/^\d+$/.test(proxy.name)) {
|
||||
proxy.name = `${proxy.name}`;
|
||||
} else {
|
||||
try {
|
||||
if (proxy.name?.data) {
|
||||
proxy.name = Buffer.from(proxy.name.data).toString('utf8');
|
||||
} else {
|
||||
proxy.name = utf8ArrayToStr(proxy.name);
|
||||
}
|
||||
} catch (e) {
|
||||
$.error(`proxy.name decode failed\nReason: ${e}`);
|
||||
proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;
|
||||
}
|
||||
} catch (e) {
|
||||
$.error(`proxy.name decode failed\nReason: ${e}`);
|
||||
proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;
|
||||
}
|
||||
}
|
||||
if (['', 'off'].includes(proxy.sni)) {
|
||||
|
||||
@@ -674,6 +674,89 @@ function URI_TUIC() {
|
||||
};
|
||||
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
|
||||
function URI_Trojan() {
|
||||
@@ -913,6 +996,15 @@ function Loon_Http() {
|
||||
const parse = (line) => getLoonParser().parse(line);
|
||||
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() {
|
||||
const name = 'Loon WireGuard Parser';
|
||||
@@ -1196,6 +1288,7 @@ export default [
|
||||
URI_VMess(),
|
||||
URI_VLESS(),
|
||||
URI_TUIC(),
|
||||
URI_WireGuard(),
|
||||
URI_Hysteria(),
|
||||
URI_Hysteria2(),
|
||||
URI_Trojan(),
|
||||
@@ -1218,6 +1311,7 @@ export default [
|
||||
Loon_Hysteria2(),
|
||||
Loon_Trojan(),
|
||||
Loon_Http(),
|
||||
Loon_Socks5(),
|
||||
Loon_WireGuard(),
|
||||
QX_SS(),
|
||||
QX_SSR(),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)* {
|
||||
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 {
|
||||
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); }
|
||||
|
||||
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; }
|
||||
|
||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)* {
|
||||
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 {
|
||||
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); }
|
||||
|
||||
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; }
|
||||
|
||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||
|
||||
@@ -22,6 +22,9 @@ function Base64Encoded() {
|
||||
'aHR0c', // htt
|
||||
'dmxlc3M=', // vless
|
||||
'aHlzdGVyaWEy', // hysteria2
|
||||
'd2lyZWd1YXJkOi8v', // wireguard://
|
||||
'd2c6Ly8=', // wg://
|
||||
'dHVpYzovLw==', // tuic://
|
||||
];
|
||||
|
||||
const test = function (raw) {
|
||||
|
||||
@@ -18,6 +18,8 @@ export default function Loon_Producer() {
|
||||
return vless(proxy);
|
||||
case 'http':
|
||||
return http(proxy);
|
||||
case 'socks5':
|
||||
return socks5(proxy);
|
||||
case 'wireguard':
|
||||
return wireguard(proxy);
|
||||
case 'hysteria2':
|
||||
@@ -316,6 +318,29 @@ function http(proxy) {
|
||||
|
||||
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) {
|
||||
if (Array.isArray(proxy.peers) && proxy.peers.length > 0) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { isPresent, Result } from './utils';
|
||||
const targetPlatform = 'QX';
|
||||
|
||||
export default function QX_Producer() {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const produce = (proxy, type, opts = {}) => {
|
||||
switch (proxy.type) {
|
||||
case 'ss':
|
||||
@@ -18,13 +19,7 @@ export default function QX_Producer() {
|
||||
case 'socks5':
|
||||
return socks5(proxy);
|
||||
case 'vless':
|
||||
if (opts['include-unsupported-proxy']) {
|
||||
return vless(proxy);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Platform ${targetPlatform}(App Store Release) does not support proxy type: ${proxy.type}`,
|
||||
);
|
||||
}
|
||||
return vless(proxy);
|
||||
}
|
||||
throw new Error(
|
||||
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ClashMeta_Producer from './clashmeta';
|
||||
import $ from '@/core/app';
|
||||
import { isIPv4, isIPv6 } from '@/utils';
|
||||
|
||||
const detourParser = (proxy, parsedProxy) => {
|
||||
if (proxy['dialer-proxy']) parsedProxy.detour = proxy['dialer-proxy'];
|
||||
@@ -620,8 +621,11 @@ const tuic5Parser = (proxy = {}) => {
|
||||
const wireguardParser = (proxy = {}) => {
|
||||
const local_address = ['ip', 'ipv6']
|
||||
.map((i) => proxy[i])
|
||||
.filter((i) => i)
|
||||
.map((i) => (/\\/.test(i) ? i : `${i}/32`));
|
||||
.map((i) => {
|
||||
if (isIPv4(i)) return `${i}/32`;
|
||||
if (isIPv6(i)) return `${i}/128`;
|
||||
})
|
||||
.filter((i) => i);
|
||||
const parsedProxy = {
|
||||
tag: proxy.name,
|
||||
type: 'wireguard',
|
||||
|
||||
@@ -411,8 +411,51 @@ export default function URI_Producer() {
|
||||
}?${tuicParams.join('&')}#${encodeURIComponent(
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getPlatformFromHeaders } from '@/utils/user-agent';
|
||||
import { ProxyUtils } from '@/core/proxy-utils';
|
||||
import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
|
||||
import { findByName } from '@/utils/database';
|
||||
import { getFlowHeaders } from '@/utils/flow';
|
||||
@@ -29,11 +30,27 @@ export default function register($app) {
|
||||
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) {
|
||||
let { name } = req.params;
|
||||
let { name, nezhaIndex } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
nezhaIndex = decodeURIComponent(nezhaIndex);
|
||||
|
||||
const platform =
|
||||
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
|
||||
@@ -155,6 +172,15 @@ async function downloadSubscription(req, res) {
|
||||
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(
|
||||
output,
|
||||
@@ -192,8 +218,9 @@ async function downloadSubscription(req, res) {
|
||||
}
|
||||
|
||||
async function downloadCollection(req, res) {
|
||||
let { name } = req.params;
|
||||
let { name, nezhaIndex } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
nezhaIndex = decodeURIComponent(nezhaIndex);
|
||||
|
||||
const platform =
|
||||
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
|
||||
@@ -300,6 +327,15 @@ async function downloadCollection(req, res) {
|
||||
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(
|
||||
output,
|
||||
@@ -338,6 +374,85 @@ 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,
|
||||
@@ -376,7 +491,7 @@ function nezhaTransform(output) {
|
||||
Virtualization: '',
|
||||
BootTime: time,
|
||||
CountryCode, // 目前需要
|
||||
Version: '',
|
||||
Version: '0.0.1',
|
||||
},
|
||||
status: {
|
||||
CPU: 0,
|
||||
|
||||
@@ -181,11 +181,21 @@ function createSubscription(req, res) {
|
||||
|
||||
function getSubscription(req, res) {
|
||||
let { name } = req.params;
|
||||
let { raw } = req.query;
|
||||
name = decodeURIComponent(name);
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const sub = findByName(allSubs, name);
|
||||
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 {
|
||||
failed(
|
||||
res,
|
||||
|
||||
@@ -40,7 +40,7 @@ const ISOFlags = {
|
||||
'🇭🇷': ['HR', 'HRV'],
|
||||
'🇭🇺': ['HU', 'HUN'],
|
||||
'🇯🇴': ['JO', 'JOR'],
|
||||
'🇯🇵': ['JP', 'JPN'],
|
||||
'🇯🇵': ['JP', 'JPN', 'TYO'],
|
||||
'🇰🇪': ['KE', 'KEN'],
|
||||
'🇰🇬': ['KG', 'KGZ'],
|
||||
'🇰🇭': ['KH', 'KGZ'],
|
||||
|
||||
79
scripts/ip-flag-node.js
Normal file
79
scripts/ip-flag-node.js
Normal 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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user