mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d073dfeef8 | ||
|
|
f970ea3361 | ||
|
|
630bac0575 | ||
|
|
7f3cb2b191 | ||
|
|
92e1e4a0fb | ||
|
|
3b85d313fe | ||
|
|
c91d8e28e4 | ||
|
|
8cbb4492be | ||
|
|
6f7da57e3a | ||
|
|
2586c29746 | ||
|
|
6f7fe8204b | ||
|
|
bafaf07743 | ||
|
|
9962eb0947 | ||
|
|
ac5232a7bc | ||
|
|
2301ccbfb5 | ||
|
|
0b5761e5fc | ||
|
|
3ab21b0e26 | ||
|
|
89ab72e46c | ||
|
|
18bd6526d0 | ||
|
|
c7329c32eb | ||
|
|
4819ae95e4 | ||
|
|
370d228b04 | ||
|
|
d092916168 |
8
.github/workflows/main.yml
vendored
8
.github/workflows/main.yml
vendored
@@ -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
|
||||
|
||||
@@ -11,7 +11,7 @@ Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.
|
||||
</p>
|
||||
|
||||
[](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml)     
|
||||
|
||||
<a href="https://trendshift.io/repositories/4572" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4572" alt="sub-store-org%2FSub-Store | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
[](https://www.buymeacoffee.com/PengYM)
|
||||
|
||||
Core functionalities:
|
||||
@@ -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.286",
|
||||
"version": "2.14.308",
|
||||
"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",
|
||||
|
||||
@@ -16,6 +16,7 @@ import { FILES_KEY, MODULES_KEY } from '@/constants';
|
||||
import { findByName } from '@/utils/database';
|
||||
import { produceArtifact } from '@/restful/sync';
|
||||
import { getFlag, getISO } from '@/utils/geo';
|
||||
import Gist from '@/utils/gist';
|
||||
|
||||
function preprocess(raw) {
|
||||
for (const processor of PROXY_PREPROCESSORS) {
|
||||
@@ -219,7 +220,7 @@ function produce(proxies, targetPlatform, type, opts = {}) {
|
||||
$.log(`Producing proxies for target: ${targetPlatform}`);
|
||||
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
|
||||
let localPort = 10000;
|
||||
const list = proxies
|
||||
let list = proxies
|
||||
.map((proxy) => {
|
||||
try {
|
||||
let line = producer.produce(proxy, type, opts);
|
||||
@@ -245,7 +246,18 @@ function produce(proxies, targetPlatform, type, opts = {}) {
|
||||
}
|
||||
})
|
||||
.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') {
|
||||
return producer.produce(proxies, type, opts);
|
||||
}
|
||||
@@ -261,6 +273,7 @@ export const ProxyUtils = {
|
||||
yaml: YAML,
|
||||
getFlag,
|
||||
getISO,
|
||||
Gist,
|
||||
};
|
||||
|
||||
function tryParse(parser, line) {
|
||||
@@ -404,15 +417,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)) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Base64 } from 'js-base64';
|
||||
// Parse SS URI format (only supports new SIP002, legacy format is depreciated).
|
||||
// reference: https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme
|
||||
function URI_SS() {
|
||||
// TODO: 暂不支持 httpupgrade
|
||||
const name = 'URI SS Parser';
|
||||
const test = (line) => {
|
||||
return /^ss:\/\//.test(line);
|
||||
@@ -299,6 +300,7 @@ function URI_VMess() {
|
||||
if (proxy.tls && proxy.sni) {
|
||||
proxy.sni = params.sni;
|
||||
}
|
||||
let httpupgrade = false;
|
||||
// handle obfs
|
||||
if (params.net === 'ws' || params.obfs === 'websocket') {
|
||||
proxy.network = 'ws';
|
||||
@@ -309,6 +311,12 @@ function URI_VMess() {
|
||||
proxy.network = 'http';
|
||||
} else if (['grpc'].includes(params.net)) {
|
||||
proxy.network = 'grpc';
|
||||
} else if (
|
||||
params.net === 'httpupgrade' ||
|
||||
proxy.network === 'httpupgrade'
|
||||
) {
|
||||
proxy.network = 'ws';
|
||||
httpupgrade = true;
|
||||
}
|
||||
if (proxy.network) {
|
||||
let transportHost = params.host ?? params.obfsParam;
|
||||
@@ -341,10 +349,15 @@ function URI_VMess() {
|
||||
'_grpc-type': getIfNotBlank(params.type),
|
||||
};
|
||||
} else {
|
||||
proxy[`${proxy.network}-opts`] = {
|
||||
const opts = {
|
||||
path: getIfNotBlank(transportPath),
|
||||
headers: { Host: getIfNotBlank(transportHost) },
|
||||
};
|
||||
if (httpupgrade) {
|
||||
opts['v2ray-http-upgrade'] = true;
|
||||
opts['v2ray-http-upgrade-fast-open'] = true;
|
||||
}
|
||||
proxy[`${proxy.network}-opts`] = opts;
|
||||
}
|
||||
} else {
|
||||
delete proxy.network;
|
||||
@@ -444,10 +457,13 @@ function URI_VLESS() {
|
||||
proxy[`${params.security}-opts`] = opts;
|
||||
}
|
||||
}
|
||||
|
||||
let httpupgrade = false;
|
||||
proxy.network = params.type;
|
||||
if (proxy.network === 'tcp' && params.headerType === 'http') {
|
||||
proxy.network = 'http';
|
||||
} else if (proxy.network === 'httpupgrade') {
|
||||
proxy.network = 'ws';
|
||||
httpupgrade = true;
|
||||
}
|
||||
if (!proxy.network && isShadowrocket && params.obfs) {
|
||||
proxy.network = params.obfs;
|
||||
@@ -485,6 +501,10 @@ function URI_VLESS() {
|
||||
if (['grpc'].includes(proxy.network)) {
|
||||
opts['_grpc-type'] = params.mode || 'gun';
|
||||
}
|
||||
if (httpupgrade) {
|
||||
opts['v2ray-http-upgrade'] = true;
|
||||
opts['v2ray-http-upgrade-fast-open'] = true;
|
||||
}
|
||||
if (Object.keys(opts).length > 0) {
|
||||
proxy[`${proxy.network}-opts`] = opts;
|
||||
}
|
||||
@@ -996,6 +1016,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';
|
||||
@@ -1302,6 +1331,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; }
|
||||
|
||||
@@ -50,8 +50,11 @@ trojan = "trojan" equals address
|
||||
|
||||
shadowsocks = "shadowsocks" equals address
|
||||
(password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/server_check_url/others)* {
|
||||
if (proxy.protocol) {
|
||||
if (proxy.protocol || proxy.type === "ssr") {
|
||||
proxy.type = "ssr";
|
||||
if (!proxy.protocol) {
|
||||
proxy.protocol = "origin";
|
||||
}
|
||||
// handle ssr obfs
|
||||
if (obfs.host) proxy["obfs-param"] = obfs.host;
|
||||
if (obfs.type) proxy.obfs = obfs.type;
|
||||
@@ -172,7 +175,7 @@ tls_no_session_reuse = comma "tls-no-session-reuse" equals flag:bool {
|
||||
}
|
||||
|
||||
obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; }
|
||||
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; return type; }
|
||||
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { proxy.type = "ssr"; obfs.type = type; return type; }
|
||||
obfs = comma "obfs" equals type:("wss"/"ws"/"over-tls"/"http") { obfs.type = type; return type; };
|
||||
|
||||
obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; }
|
||||
|
||||
@@ -48,8 +48,11 @@ trojan = "trojan" equals address
|
||||
|
||||
shadowsocks = "shadowsocks" equals address
|
||||
(password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/server_check_url/others)* {
|
||||
if (proxy.protocol) {
|
||||
if (proxy.protocol || proxy.type === "ssr") {
|
||||
proxy.type = "ssr";
|
||||
if (!proxy.protocol) {
|
||||
proxy.protocol = "origin";
|
||||
}
|
||||
// handle ssr obfs
|
||||
if (obfs.host) proxy["obfs-param"] = obfs.host;
|
||||
if (obfs.type) proxy.obfs = obfs.type;
|
||||
@@ -170,7 +173,7 @@ tls_no_session_reuse = comma "tls-no-session-reuse" equals flag:bool {
|
||||
}
|
||||
|
||||
obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; }
|
||||
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; return type; }
|
||||
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { proxy.type = "ssr"; obfs.type = type; return type; }
|
||||
obfs = comma "obfs" equals type:("wss"/"ws"/"over-tls"/"http") { obfs.type = type; return type; };
|
||||
|
||||
obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; }
|
||||
|
||||
@@ -89,7 +89,12 @@ params = "?" head:param tail:("&"@param)* {
|
||||
}
|
||||
|
||||
if (params["type"]) {
|
||||
let httpupgrade
|
||||
proxy.network = params["type"]
|
||||
if(proxy.network === 'httpupgrade') {
|
||||
proxy.network = 'ws'
|
||||
httpupgrade = true
|
||||
}
|
||||
if (['grpc'].includes(proxy.network)) {
|
||||
proxy[proxy.network + '-opts'] = {
|
||||
'grpc-service-name': params["serviceName"],
|
||||
@@ -102,6 +107,10 @@ params = "?" head:param tail:("&"@param)* {
|
||||
if (params["host"]) {
|
||||
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
|
||||
}
|
||||
if (httpupgrade) {
|
||||
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade", true);
|
||||
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,12 @@ params = "?" head:param tail:("&"@param)* {
|
||||
}
|
||||
|
||||
if (params["type"]) {
|
||||
let httpupgrade
|
||||
proxy.network = params["type"]
|
||||
if(proxy.network === 'httpupgrade') {
|
||||
proxy.network = 'ws'
|
||||
httpupgrade = true
|
||||
}
|
||||
if (['grpc'].includes(proxy.network)) {
|
||||
proxy[proxy.network + '-opts'] = {
|
||||
'grpc-service-name': params["serviceName"],
|
||||
@@ -100,6 +105,10 @@ params = "?" head:param tail:("&"@param)* {
|
||||
if (params["host"]) {
|
||||
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
|
||||
}
|
||||
if (httpupgrade) {
|
||||
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade", true);
|
||||
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -867,7 +867,7 @@ function clone(object) {
|
||||
// remove flag
|
||||
function removeFlag(str) {
|
||||
return str
|
||||
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, '')
|
||||
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]|🏴☠️|🏳️🌈/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -762,8 +762,8 @@ function wireguard(proxy) {
|
||||
}
|
||||
const result = new Result(proxy);
|
||||
|
||||
result.append(`# WireGuard Proxy ${proxy.name}
|
||||
${proxy.name}=wireguard`);
|
||||
result.append(`# > WireGuard Proxy ${proxy.name}
|
||||
# ${proxy.name}=wireguard`);
|
||||
|
||||
proxy['section-name'] = getIfNotBlank(proxy['section-name'], proxy.name);
|
||||
|
||||
@@ -821,7 +821,7 @@ ${proxy.name}=wireguard`);
|
||||
);
|
||||
|
||||
result.append(`
|
||||
# WireGuard Section ${proxy.name}
|
||||
# > WireGuard Section ${proxy.name}
|
||||
[WireGuard ${proxy['section-name']}]
|
||||
private-key = ${proxy['private-key']}`);
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function URI_Producer() {
|
||||
const userinfo = `${proxy.cipher}:${proxy.password}`;
|
||||
result = `ss://${Base64.encode(userinfo)}@${proxy.server}:${
|
||||
proxy.port
|
||||
}/`;
|
||||
}${proxy.plugin ? '/' : ''}`;
|
||||
if (proxy.plugin) {
|
||||
result += '?plugin=';
|
||||
const opts = proxy['plugin-opts'];
|
||||
@@ -55,7 +55,9 @@ export default function URI_Producer() {
|
||||
result = `${result}${proxy.plugin ? '&' : '?'}uot=1`;
|
||||
}
|
||||
if (proxy.tfo) {
|
||||
result = `${result}${proxy.plugin ? '&' : '?'}tfo=1`;
|
||||
result = `${result}${
|
||||
proxy.plugin || proxy['udp-over-tcp'] ? '&' : '?'
|
||||
}tfo=1`;
|
||||
}
|
||||
result += `#${encodeURIComponent(proxy.name)}`;
|
||||
break;
|
||||
@@ -82,6 +84,11 @@ export default function URI_Producer() {
|
||||
if (proxy.network === 'http') {
|
||||
net = 'tcp';
|
||||
type = 'http';
|
||||
} else if (
|
||||
proxy.network === 'ws' &&
|
||||
proxy['ws-opts']?.['v2ray-http-upgrade']
|
||||
) {
|
||||
net = 'httpupgrade';
|
||||
}
|
||||
result = {
|
||||
v: '2',
|
||||
@@ -170,9 +177,15 @@ export default function URI_Producer() {
|
||||
if (proxy.flow) {
|
||||
flow = `&flow=${encodeURIComponent(proxy.flow)}`;
|
||||
}
|
||||
let vlessTransport = `&type=${encodeURIComponent(
|
||||
proxy.network,
|
||||
)}`;
|
||||
let vlessType = proxy.network;
|
||||
if (
|
||||
proxy.network === 'ws' &&
|
||||
proxy['ws-opts']?.['v2ray-http-upgrade']
|
||||
) {
|
||||
vlessType = 'httpupgrade';
|
||||
}
|
||||
|
||||
let vlessTransport = `&type=${encodeURIComponent(vlessType)}`;
|
||||
if (['grpc'].includes(proxy.network)) {
|
||||
// https://github.com/XTLS/Xray-core/issues/91
|
||||
vlessTransport += `&mode=${encodeURIComponent(
|
||||
@@ -218,7 +231,14 @@ export default function URI_Producer() {
|
||||
case 'trojan':
|
||||
let trojanTransport = '';
|
||||
if (proxy.network) {
|
||||
trojanTransport = `&type=${proxy.network}`;
|
||||
let trojanType = proxy.network;
|
||||
if (
|
||||
proxy.network === 'ws' &&
|
||||
proxy['ws-opts']?.['v2ray-http-upgrade']
|
||||
) {
|
||||
trojanType = 'httpupgrade';
|
||||
}
|
||||
trojanTransport = `&type=${encodeURIComponent(trojanType)}`;
|
||||
if (['grpc'].includes(proxy.network)) {
|
||||
let trojanTransportServiceName =
|
||||
proxy[`${proxy.network}-opts`]?.[
|
||||
|
||||
@@ -253,7 +253,25 @@ async function syncToGist(files) {
|
||||
key: ARTIFACT_REPOSITORY_KEY,
|
||||
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 };
|
||||
|
||||
@@ -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,
|
||||
@@ -354,7 +469,11 @@ function nezhaTransform(output) {
|
||||
// 简单判断下
|
||||
if (/^[a-z]{2}$/i.test(CountryCode)) {
|
||||
// 如果节点上有数据 就取节点上的数据
|
||||
let time = proxy._unavailable ? 0 : Date.now();
|
||||
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,
|
||||
@@ -374,9 +493,9 @@ function nezhaTransform(output) {
|
||||
SwapTotal: 1024,
|
||||
Arch: '',
|
||||
Virtualization: '',
|
||||
BootTime: time,
|
||||
BootTime: now - uptime,
|
||||
CountryCode, // 目前需要
|
||||
Version: '',
|
||||
Version: '0.0.1',
|
||||
},
|
||||
status: {
|
||||
CPU: 0,
|
||||
@@ -387,7 +506,7 @@ function nezhaTransform(output) {
|
||||
NetOutTransfer: 0,
|
||||
NetInSpeed: 0,
|
||||
NetOutSpeed: 0,
|
||||
Uptime: parseInt(proxy._uptime ?? index, 10),
|
||||
Uptime: uptime,
|
||||
Load1: 0,
|
||||
Load5: 0,
|
||||
Load15: 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,
|
||||
|
||||
@@ -135,9 +135,9 @@ export default class Gist {
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log(`result`, result);
|
||||
console.log(`files`, files);
|
||||
console.log(`actions`, actions);
|
||||
// console.log(`result`, result);
|
||||
// console.log(`files`, files);
|
||||
// console.log(`actions`, actions);
|
||||
|
||||
if (this.syncPlatform === 'gitlab') {
|
||||
if (Object.keys(result).length === 0) {
|
||||
|
||||
@@ -39,6 +39,7 @@ function operator(proxies = [], targetPlatform, context) {
|
||||
// yaml, // yaml 解析和生成
|
||||
// getFlag, // 获取 emoji 旗帜
|
||||
// getISO, // 获取 ISO 3166-1 alpha-2 代码
|
||||
// Gist, // Gist 类
|
||||
// }
|
||||
|
||||
// 示例: 给节点名添加前缀
|
||||
@@ -94,6 +95,42 @@ function operator(proxies = [], targetPlatform, context) {
|
||||
// produceType: 'internal' // 'internal' produces an Array, otherwise produces a String( ProxyUtils.yaml.safeLoad('YAML String').proxies )
|
||||
// })
|
||||
|
||||
// 4. 一个比较折腾的方案: 在脚本操作中, 把内容同步到另一个 gist
|
||||
|
||||
// async function operator(proxies = []) {
|
||||
// const $ = $substore
|
||||
// const GITHUB_TOKEN = 'ghp_xxxxxxxxxxxxxxxxxxxxx'
|
||||
// const GIST_NAME = 'share'
|
||||
// const FILENAME = 'mihomo.yaml'
|
||||
// let files = {}
|
||||
// let content = await produceArtifact({
|
||||
// type: 'subscription',
|
||||
// subscription: {},
|
||||
// content: 'proxies:\n' + proxies.map((proxy) => ' - ' + JSON.stringify(proxy) + '\n').join(''),
|
||||
// platform: 'ClashMeta',
|
||||
// })
|
||||
// const manager = new ProxyUtils.Gist({
|
||||
// token: GITHUB_TOKEN,
|
||||
// key: GIST_NAME,
|
||||
// });
|
||||
// files[encodeURIComponent(FILENAME)] = {
|
||||
// content,
|
||||
// };
|
||||
// const res = await manager.upload(files);
|
||||
// let body = {};
|
||||
// try {
|
||||
// body = JSON.parse(res.body);
|
||||
// // eslint-disable-next-line no-empty
|
||||
// } catch (e) {}
|
||||
// const raw_url =
|
||||
// body.files[encodeURIComponent(FILENAME)]?.raw_url;
|
||||
// console.log(raw_url)
|
||||
// const new_url = raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
|
||||
// console.log(new_url)
|
||||
// $.notify('🌍 Sub-Store', `更新到 Gist: ${new_url}`);
|
||||
// return proxies
|
||||
// }
|
||||
|
||||
// // YAML
|
||||
// ProxyUtils.yaml.load('YAML String')
|
||||
// ProxyUtils.yaml.safeLoad('YAML String')
|
||||
|
||||
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