Compare commits

..

10 Commits

12 changed files with 306 additions and 16 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.14.282",
"version": "2.14.292",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js",
"scripts": {

View File

@@ -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(),

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;
}
@@ -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; }

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;
}
@@ -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; }

View File

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

View File

@@ -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) {

View File

@@ -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',

View File

@@ -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;
};

View File

@@ -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,

View File

@@ -40,7 +40,7 @@ const ISOFlags = {
'🇭🇷': ['HR', 'HRV'],
'🇭🇺': ['HU', 'HUN'],
'🇯🇴': ['JO', 'JOR'],
'🇯🇵': ['JP', 'JPN'],
'🇯🇵': ['JP', 'JPN', 'TYO'],
'🇰🇪': ['KE', 'KEN'],
'🇰🇬': ['KG', 'KGZ'],
'🇰🇭': ['KH', 'KGZ'],