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 | |
|---|---|---|---|
|
|
a91f978042 | ||
|
|
1248e6b32a | ||
|
|
d0f255d9c6 | ||
|
|
060415584e | ||
|
|
8a2087c53a | ||
|
|
64117c50c7 | ||
|
|
6564d9497a | ||
|
|
1c03e46bbb | ||
|
|
084b385fdb | ||
|
|
290b9b5411 | ||
|
|
e5c1ae9ed8 | ||
|
|
9b6d9d49f9 | ||
|
|
a12adf5255 | ||
|
|
8682f14ee7 | ||
|
|
b3de7a4bc5 | ||
|
|
099ae5ad83 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sub-store",
|
||||
"version": "2.14.185",
|
||||
"version": "2.14.195",
|
||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -237,6 +237,7 @@ function lastParse(proxy) {
|
||||
delete proxy['ws-path'];
|
||||
delete proxy['ws-headers'];
|
||||
}
|
||||
|
||||
if (proxy.type === 'trojan') {
|
||||
if (proxy.network === 'tcp') {
|
||||
delete proxy.network;
|
||||
@@ -253,11 +254,26 @@ function lastParse(proxy) {
|
||||
if (proxy.network) {
|
||||
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
let transporthost = proxy[`${proxy.network}-opts`]?.headers?.host;
|
||||
if (transporthost && !transportHost) {
|
||||
if (proxy.network === 'h2') {
|
||||
if (!transporthost && transportHost) {
|
||||
proxy[`${proxy.network}-opts`].headers.host = transportHost;
|
||||
delete proxy[`${proxy.network}-opts`].headers.Host;
|
||||
}
|
||||
} else if (transporthost && !transportHost) {
|
||||
proxy[`${proxy.network}-opts`].headers.Host = transporthost;
|
||||
delete proxy[`${proxy.network}-opts`].headers.host;
|
||||
}
|
||||
}
|
||||
if (proxy.network === 'h2') {
|
||||
const host = proxy['h2-opts']?.headers?.host;
|
||||
const path = proxy['h2-opts']?.path;
|
||||
if (host && !Array.isArray(host)) {
|
||||
proxy['h2-opts'].headers.host = [host];
|
||||
}
|
||||
if (Array.isArray(path)) {
|
||||
proxy['h2-opts'].path = path[0];
|
||||
}
|
||||
}
|
||||
if (proxy.tls && !proxy.sni) {
|
||||
if (proxy.network) {
|
||||
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
|
||||
@@ -32,8 +32,27 @@ function URI_SS() {
|
||||
// handle IPV4 and IPV6
|
||||
let serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
|
||||
let userInfoStr = Base64.decode(content.split('@')[0]);
|
||||
let query = '';
|
||||
if (!serverAndPortArray) {
|
||||
if (content.includes('?')) {
|
||||
const parsed = content.match(/^(.*)(\?.*)$/);
|
||||
content = parsed[1];
|
||||
query = parsed[2];
|
||||
}
|
||||
content = Base64.decode(content);
|
||||
if (query) {
|
||||
if (/(&|\?)v2ray-plugin=/.test(query)) {
|
||||
const parsed = query.match(/(&|\?)v2ray-plugin=(.*?)(&|$)/);
|
||||
let v2rayPlugin = parsed[2];
|
||||
if (v2rayPlugin) {
|
||||
proxy.plugin = 'v2ray-plugin';
|
||||
proxy['plugin-opts'] = JSON.parse(
|
||||
Base64.decode(v2rayPlugin),
|
||||
);
|
||||
}
|
||||
}
|
||||
content = `${content}${query}`;
|
||||
}
|
||||
userInfoStr = content.split('@')[0];
|
||||
serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
|
||||
}
|
||||
@@ -70,7 +89,7 @@ function URI_SS() {
|
||||
};
|
||||
break;
|
||||
case 'v2ray-plugin':
|
||||
proxy.obfs = 'v2ray-plugin';
|
||||
proxy.plugin = 'v2ray-plugin';
|
||||
proxy['plugin-opts'] = {
|
||||
mode: 'websocket',
|
||||
host: getIfNotBlank(params['obfs-host']),
|
||||
@@ -84,6 +103,12 @@ function URI_SS() {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (/(&|\?)uot=(1|true)/i.test(query)) {
|
||||
proxy['udp-over-tcp'] = true;
|
||||
}
|
||||
if (/(&|\?)tfo=(1|true)/i.test(query)) {
|
||||
proxy.tfo = true;
|
||||
}
|
||||
return proxy;
|
||||
};
|
||||
return { name, test, parse };
|
||||
@@ -416,16 +441,31 @@ function URI_VLESS() {
|
||||
if (!proxy.network && isShadowrocket && params.obfs) {
|
||||
proxy.network = params.obfs;
|
||||
}
|
||||
if (['websocket'].includes(proxy.network)) {
|
||||
proxy.network = 'ws';
|
||||
}
|
||||
if (proxy.network && !['tcp', 'none'].includes(proxy.network)) {
|
||||
const opts = {};
|
||||
if (params.host) {
|
||||
opts.headers = { Host: params.host };
|
||||
const host = params.host ?? params.obfsParam;
|
||||
if (host) {
|
||||
if (params.obfsParam) {
|
||||
try {
|
||||
const parsed = JSON.parse(host);
|
||||
opts.headers = parsed;
|
||||
} catch (e) {
|
||||
opts.headers = { Host: host };
|
||||
}
|
||||
} else {
|
||||
opts.headers = { Host: host };
|
||||
}
|
||||
}
|
||||
if (params.serviceName) {
|
||||
opts[`${proxy.network}-service-name`] = params.serviceName;
|
||||
} else if (isShadowrocket && params.path) {
|
||||
opts[`${proxy.network}-service-name`] = params.path;
|
||||
delete params.path;
|
||||
if (!['ws', 'http', 'h2'].includes(proxy.network)) {
|
||||
opts[`${proxy.network}-service-name`] = params.path;
|
||||
delete params.path;
|
||||
}
|
||||
}
|
||||
if (params.path) {
|
||||
opts.path = params.path;
|
||||
|
||||
@@ -30,6 +30,11 @@ const grammars = String.raw`
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleShadowTLS() {
|
||||
if (proxy['shadow-tls-password'] && !proxy['shadow-tls-version']) {
|
||||
proxy['shadow-tls-version'] = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
|
||||
@@ -45,6 +50,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
|
||||
$set(proxy, "plugin-opts.host", obfs.host);
|
||||
$set(proxy, "plugin-opts.path", obfs.path);
|
||||
}
|
||||
handleShadowTLS();
|
||||
}
|
||||
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "vmess";
|
||||
@@ -55,17 +61,21 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
|
||||
proxy.alterId = proxy.alterId || 0;
|
||||
}
|
||||
handleWebsocket();
|
||||
handleShadowTLS();
|
||||
}
|
||||
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "trojan";
|
||||
handleWebsocket();
|
||||
handleShadowTLS();
|
||||
}
|
||||
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "http";
|
||||
proxy.tls = true;
|
||||
handleShadowTLS();
|
||||
}
|
||||
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "http";
|
||||
handleShadowTLS();
|
||||
}
|
||||
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "snell";
|
||||
@@ -75,26 +85,33 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
|
||||
$set(proxy, "obfs-opts.host", obfs.host);
|
||||
$set(proxy, "obfs-opts.path", obfs.path);
|
||||
}
|
||||
handleShadowTLS();
|
||||
}
|
||||
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "tuic";
|
||||
handleShadowTLS();
|
||||
}
|
||||
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "tuic";
|
||||
proxy.version = 5;
|
||||
handleShadowTLS();
|
||||
}
|
||||
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "wireguard-surge";
|
||||
handleShadowTLS();
|
||||
}
|
||||
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "hysteria2";
|
||||
handleShadowTLS();
|
||||
}
|
||||
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "socks5";
|
||||
handleShadowTLS();
|
||||
}
|
||||
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "socks5";
|
||||
proxy.tls = true;
|
||||
handleShadowTLS();
|
||||
}
|
||||
|
||||
address = comma server:server comma port:port {
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleShadowTLS() {
|
||||
if (proxy['shadow-tls-password'] && !proxy['shadow-tls-version']) {
|
||||
proxy['shadow-tls-version'] = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
|
||||
@@ -43,6 +48,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
|
||||
$set(proxy, "plugin-opts.host", obfs.host);
|
||||
$set(proxy, "plugin-opts.path", obfs.path);
|
||||
}
|
||||
handleShadowTLS();
|
||||
}
|
||||
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "vmess";
|
||||
@@ -53,17 +59,21 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
|
||||
proxy.alterId = proxy.alterId || 0;
|
||||
}
|
||||
handleWebsocket();
|
||||
handleShadowTLS();
|
||||
}
|
||||
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "trojan";
|
||||
handleWebsocket();
|
||||
handleShadowTLS();
|
||||
}
|
||||
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "http";
|
||||
proxy.tls = true;
|
||||
handleShadowTLS();
|
||||
}
|
||||
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "http";
|
||||
handleShadowTLS();
|
||||
}
|
||||
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "snell";
|
||||
@@ -73,26 +83,33 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
|
||||
$set(proxy, "obfs-opts.host", obfs.host);
|
||||
$set(proxy, "obfs-opts.path", obfs.path);
|
||||
}
|
||||
handleShadowTLS();
|
||||
}
|
||||
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "tuic";
|
||||
handleShadowTLS();
|
||||
}
|
||||
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "tuic";
|
||||
proxy.version = 5;
|
||||
handleShadowTLS();
|
||||
}
|
||||
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "wireguard-surge";
|
||||
handleShadowTLS();
|
||||
}
|
||||
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "hysteria2";
|
||||
handleShadowTLS();
|
||||
}
|
||||
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "socks5";
|
||||
handleShadowTLS();
|
||||
}
|
||||
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "socks5";
|
||||
proxy.tls = true;
|
||||
handleShadowTLS();
|
||||
}
|
||||
|
||||
address = comma server:server comma port:port {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
getFlowField,
|
||||
getFlowHeaders,
|
||||
parseFlowHeaders,
|
||||
validCheck,
|
||||
flowTransfer,
|
||||
} from '@/utils/flow';
|
||||
|
||||
@@ -324,7 +325,7 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
|
||||
const operator = createDynamicFunction(
|
||||
'operator',
|
||||
`async function operator(input = []) {
|
||||
if (input?.$files || input?.$content) {
|
||||
if (input && (input.$files || input.$content)) {
|
||||
let { $content, $files } = input
|
||||
${script}
|
||||
return { $content, $files }
|
||||
@@ -806,6 +807,7 @@ function createDynamicFunction(name, script, $arguments) {
|
||||
getFlowHeaders,
|
||||
parseFlowHeaders,
|
||||
flowTransfer,
|
||||
validCheck,
|
||||
};
|
||||
if ($.env.isLoon) {
|
||||
return new Function(
|
||||
|
||||
@@ -107,6 +107,31 @@ export default function Clash_Producer() {
|
||||
proxy['http-opts'].headers.Host = [httpHost];
|
||||
}
|
||||
}
|
||||
if (
|
||||
['vmess', 'vless'].includes(proxy.type) &&
|
||||
proxy.network === 'h2'
|
||||
) {
|
||||
let path = proxy['h2-opts']?.path;
|
||||
if (
|
||||
isPresent(proxy, 'h2-opts.path') &&
|
||||
Array.isArray(path)
|
||||
) {
|
||||
proxy['h2-opts'].path = path[0];
|
||||
}
|
||||
let host = proxy['h2-opts']?.headers?.host;
|
||||
if (
|
||||
isPresent(proxy, 'h2-opts.headers.Host') &&
|
||||
!Array.isArray(host)
|
||||
) {
|
||||
proxy['h2-opts'].headers.host = [host];
|
||||
}
|
||||
}
|
||||
if (proxy['plugin-opts']?.tls) {
|
||||
if (isPresent(proxy, 'skip-cert-verify')) {
|
||||
proxy['plugin-opts']['skip-cert-verify'] =
|
||||
proxy['skip-cert-verify'];
|
||||
}
|
||||
}
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||
proxy.type,
|
||||
|
||||
@@ -89,6 +89,18 @@ export default function ClashMeta_Producer() {
|
||||
proxy.servername = proxy.sni;
|
||||
delete proxy.sni;
|
||||
}
|
||||
} else if (proxy.type === 'ss') {
|
||||
if (
|
||||
isPresent(proxy, 'shadow-tls-password') &&
|
||||
!isPresent(proxy, 'plugin')
|
||||
) {
|
||||
proxy.plugin = 'shadow-tls';
|
||||
proxy['plugin-opts'] = {
|
||||
host: proxy['shadow-tls-sni'],
|
||||
password: proxy['shadow-tls-password'],
|
||||
version: proxy['shadow-tls-version'],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -110,7 +122,32 @@ export default function ClashMeta_Producer() {
|
||||
proxy['http-opts'].headers.Host = [httpHost];
|
||||
}
|
||||
}
|
||||
if (
|
||||
['vmess', 'vless'].includes(proxy.type) &&
|
||||
proxy.network === 'h2'
|
||||
) {
|
||||
let path = proxy['h2-opts']?.path;
|
||||
if (
|
||||
isPresent(proxy, 'h2-opts.path') &&
|
||||
Array.isArray(path)
|
||||
) {
|
||||
proxy['h2-opts'].path = path[0];
|
||||
}
|
||||
let host = proxy['h2-opts']?.headers?.host;
|
||||
if (
|
||||
isPresent(proxy, 'h2-opts.headers.Host') &&
|
||||
!Array.isArray(host)
|
||||
) {
|
||||
proxy['h2-opts'].headers.host = [host];
|
||||
}
|
||||
}
|
||||
|
||||
if (proxy['plugin-opts']?.tls) {
|
||||
if (isPresent(proxy, 'skip-cert-verify')) {
|
||||
proxy['plugin-opts']['skip-cert-verify'] =
|
||||
proxy['skip-cert-verify'];
|
||||
}
|
||||
}
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||
proxy.type,
|
||||
|
||||
@@ -126,7 +126,31 @@ export default function ShadowRocket_Producer() {
|
||||
proxy['http-opts'].headers.Host = [httpHost];
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
['vmess', 'vless'].includes(proxy.type) &&
|
||||
proxy.network === 'h2'
|
||||
) {
|
||||
let path = proxy['h2-opts']?.path;
|
||||
if (
|
||||
isPresent(proxy, 'h2-opts.path') &&
|
||||
Array.isArray(path)
|
||||
) {
|
||||
proxy['h2-opts'].path = path[0];
|
||||
}
|
||||
let host = proxy['h2-opts']?.headers?.host;
|
||||
if (
|
||||
isPresent(proxy, 'h2-opts.headers.Host') &&
|
||||
!Array.isArray(host)
|
||||
) {
|
||||
proxy['h2-opts'].headers.host = [host];
|
||||
}
|
||||
}
|
||||
if (proxy['plugin-opts']?.tls) {
|
||||
if (isPresent(proxy, 'skip-cert-verify')) {
|
||||
proxy['plugin-opts']['skip-cert-verify'] =
|
||||
proxy['skip-cert-verify'];
|
||||
}
|
||||
}
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||
proxy.type,
|
||||
|
||||
@@ -263,6 +263,37 @@ const socks5Parser = (proxy = {}) => {
|
||||
return parsedProxy;
|
||||
};
|
||||
|
||||
const shadowTLSParser = (proxy = {}) => {
|
||||
const ssPart = {
|
||||
tag: proxy.name,
|
||||
type: 'shadowsocks',
|
||||
method: proxy.cipher,
|
||||
password: proxy.password,
|
||||
detour: `${proxy.name}_shadowtls`,
|
||||
};
|
||||
const stPart = {
|
||||
tag: `${proxy.name}_shadowtls`,
|
||||
type: 'shadowtls',
|
||||
server: proxy.server,
|
||||
server_port: parseInt(`${proxy.port}`, 10),
|
||||
version: proxy['plugin-opts'].version,
|
||||
password: proxy['plugin-opts'].password,
|
||||
tls: {
|
||||
enabled: true,
|
||||
server_name: proxy['plugin-opts'].host,
|
||||
utls: {
|
||||
enabled: true,
|
||||
fingerprint: proxy['client-fingerprint'],
|
||||
},
|
||||
},
|
||||
};
|
||||
if (stPart.server_port < 1 || stPart.server_port > 65535)
|
||||
throw '端口值非法';
|
||||
if (proxy['fast-open'] === true) stPart.udp_fragment = true;
|
||||
tfoParser(proxy, stPart);
|
||||
smuxParser(proxy.smux, ssPart);
|
||||
return { type: 'ss-with-st', ssPart, stPart };
|
||||
};
|
||||
const ssParser = (proxy = {}) => {
|
||||
const parsedProxy = {
|
||||
tag: proxy.name,
|
||||
@@ -607,9 +638,10 @@ export default function singbox_Producer() {
|
||||
break;
|
||||
case 'ss':
|
||||
if (proxy.plugin === 'shadow-tls') {
|
||||
throw new Error(
|
||||
`Platform sing-box does not support proxy type: ${proxy.type} with shadow-tls`,
|
||||
);
|
||||
const { ssPart, stPart } =
|
||||
shadowTLSParser(proxy);
|
||||
list.push(ssPart);
|
||||
list.push(stPart);
|
||||
} else {
|
||||
list.push(ssParser(proxy));
|
||||
}
|
||||
|
||||
@@ -206,6 +206,31 @@ export default function Stash_Producer() {
|
||||
proxy['http-opts'].headers.Host = [httpHost];
|
||||
}
|
||||
}
|
||||
if (
|
||||
['vmess', 'vless'].includes(proxy.type) &&
|
||||
proxy.network === 'h2'
|
||||
) {
|
||||
let path = proxy['h2-opts']?.path;
|
||||
if (
|
||||
isPresent(proxy, 'h2-opts.path') &&
|
||||
Array.isArray(path)
|
||||
) {
|
||||
proxy['h2-opts'].path = path[0];
|
||||
}
|
||||
let host = proxy['h2-opts']?.headers?.host;
|
||||
if (
|
||||
isPresent(proxy, 'h2-opts.headers.Host') &&
|
||||
!Array.isArray(host)
|
||||
) {
|
||||
proxy['h2-opts'].headers.host = [host];
|
||||
}
|
||||
}
|
||||
if (proxy['plugin-opts']?.tls) {
|
||||
if (isPresent(proxy, 'skip-cert-verify')) {
|
||||
proxy['plugin-opts']['skip-cert-verify'] =
|
||||
proxy['skip-cert-verify'];
|
||||
}
|
||||
}
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||
proxy.type,
|
||||
|
||||
@@ -6,6 +6,7 @@ const targetPlatform = 'Surfboard';
|
||||
|
||||
export default function Surfboard_Producer() {
|
||||
const produce = (proxy) => {
|
||||
proxy.name = proxy.name.replace(/=/g, '');
|
||||
switch (proxy.type) {
|
||||
case 'ss':
|
||||
return shadowsocks(proxy);
|
||||
|
||||
@@ -69,7 +69,7 @@ function shadowsocks(proxy) {
|
||||
`,obfs-uri=${proxy['plugin-opts'].path}`,
|
||||
'plugin-opts.path',
|
||||
);
|
||||
} else {
|
||||
} else if (!['shadow-tls'].includes(proxy.plugin)) {
|
||||
throw new Error(`plugin ${proxy.plugin} is not supported`);
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,24 @@ function shadowsocks(proxy) {
|
||||
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
|
||||
'shadow-tls-sni',
|
||||
);
|
||||
} else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {
|
||||
const password = proxy['plugin-opts'].password;
|
||||
const host = proxy['plugin-opts'].host;
|
||||
const version = proxy['plugin-opts'].version;
|
||||
if (password) {
|
||||
result.append(`,shadow-tls-password=${password}`);
|
||||
if (host) {
|
||||
result.append(`,shadow-tls-sni=${host}`);
|
||||
}
|
||||
if (version) {
|
||||
if (version < 2) {
|
||||
throw new Error(
|
||||
`shadow-tls version ${version} is not supported`,
|
||||
);
|
||||
}
|
||||
result.append(`,shadow-tls-version=${version}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// block-quic
|
||||
|
||||
@@ -39,6 +39,12 @@ export default function URI_Producer() {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (proxy['udp-over-tcp']) {
|
||||
result = `${result}${proxy.plugin ? '&' : '?'}uot=1`;
|
||||
}
|
||||
if (proxy.tfo) {
|
||||
result = `${result}${proxy.plugin ? '&' : '?'}tfo=1`;
|
||||
}
|
||||
result += `#${encodeURIComponent(proxy.name)}`;
|
||||
break;
|
||||
case 'ssr':
|
||||
|
||||
@@ -54,9 +54,18 @@ async function doSync() {
|
||||
if (artifact.sync) {
|
||||
artifact.updated = new Date().getTime();
|
||||
// extract real url from gist
|
||||
artifact.url = body.files[
|
||||
encodeURIComponent(artifact.name)
|
||||
]?.raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
|
||||
let files = body.files;
|
||||
let isGitLab;
|
||||
if (Array.isArray(files)) {
|
||||
isGitLab = true;
|
||||
files = Object.fromEntries(
|
||||
files.map((item) => [item.path, item]),
|
||||
);
|
||||
}
|
||||
const url = files[encodeURIComponent(artifact.name)]?.raw_url;
|
||||
artifact.url = isGitLab
|
||||
? url
|
||||
: url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,13 +35,14 @@ export default function register($app) {
|
||||
async function restoreArtifacts(_, res) {
|
||||
$.info('开始恢复远程配置...');
|
||||
try {
|
||||
const { gistToken } = $.read(SETTINGS_KEY);
|
||||
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
|
||||
if (!gistToken) {
|
||||
return Promise.reject('未设置 GitHub Token!');
|
||||
}
|
||||
const manager = new Gist({
|
||||
token: gistToken,
|
||||
key: ARTIFACT_REPOSITORY_KEY,
|
||||
syncPlatform,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -243,13 +244,14 @@ function validateArtifactName(name) {
|
||||
}
|
||||
|
||||
async function syncToGist(files) {
|
||||
const { gistToken } = $.read(SETTINGS_KEY);
|
||||
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
|
||||
if (!gistToken) {
|
||||
return Promise.reject('未设置 GitHub Token!');
|
||||
}
|
||||
const manager = new Gist({
|
||||
token: gistToken,
|
||||
key: ARTIFACT_REPOSITORY_KEY,
|
||||
syncPlatform,
|
||||
});
|
||||
return manager.upload(files);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import $ from '@/core/app';
|
||||
import { ENV } from '@/vendor/open-api';
|
||||
import { failed, success } from '@/restful/response';
|
||||
import { updateArtifactStore, updateGitHubAvatar } from '@/restful/settings';
|
||||
import { updateArtifactStore, updateAvatar } from '@/restful/settings';
|
||||
import resourceCache from '@/utils/resource-cache';
|
||||
import {
|
||||
GIST_BACKUP_FILE_NAME,
|
||||
@@ -68,7 +68,7 @@ function getEnv(req, res) {
|
||||
|
||||
async function refresh(_, res) {
|
||||
// 1. get GitHub avatar and artifact store
|
||||
await updateGitHubAvatar();
|
||||
await updateAvatar();
|
||||
await updateArtifactStore();
|
||||
|
||||
// 2. clear resource cache
|
||||
@@ -79,7 +79,7 @@ async function refresh(_, res) {
|
||||
async function gistBackup(req, res) {
|
||||
const { action } = req.query;
|
||||
// read token
|
||||
const { gistToken } = $.read(SETTINGS_KEY);
|
||||
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
|
||||
if (!gistToken) {
|
||||
failed(
|
||||
res,
|
||||
@@ -92,6 +92,7 @@ async function gistBackup(req, res) {
|
||||
const gist = new Gist({
|
||||
token: gistToken,
|
||||
key: GIST_BACKUP_KEY,
|
||||
syncPlatform,
|
||||
});
|
||||
try {
|
||||
let content;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SETTINGS_KEY, ARTIFACT_REPOSITORY_KEY } from '@/constants';
|
||||
import { success } from './response';
|
||||
import { success, failed } from './response';
|
||||
import { InternalServerError } from '@/restful/errors';
|
||||
import $ from '@/core/app';
|
||||
import Gist from '@/utils/gist';
|
||||
|
||||
@@ -10,53 +11,105 @@ export default function register($app) {
|
||||
}
|
||||
|
||||
async function getSettings(req, res) {
|
||||
let settings = $.read(SETTINGS_KEY);
|
||||
if (!settings) {
|
||||
settings = {};
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
}
|
||||
try {
|
||||
let settings = $.read(SETTINGS_KEY);
|
||||
if (!settings) {
|
||||
settings = {};
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
}
|
||||
|
||||
if (!settings.avatarUrl) await updateGitHubAvatar();
|
||||
if (!settings.artifactStore) await updateArtifactStore();
|
||||
success(res, settings);
|
||||
// TODO: 缺错误处理 前端也缺
|
||||
if (!settings.avatarUrl) await updateAvatar();
|
||||
if (!settings.artifactStore) await updateArtifactStore();
|
||||
|
||||
success(res, settings);
|
||||
} catch (e) {
|
||||
$.error(`Failed to get settings: ${e.message ?? e}`);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
`FAILED_TO_GET_SETTINGS`,
|
||||
`Failed to get settings`,
|
||||
`Reason: ${e.message ?? e}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSettings(req, res) {
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const newSettings = {
|
||||
...settings,
|
||||
...req.body,
|
||||
};
|
||||
$.write(newSettings, SETTINGS_KEY);
|
||||
await updateGitHubAvatar();
|
||||
await updateArtifactStore();
|
||||
success(res, newSettings);
|
||||
// TODO: 缺错误处理 前端也缺
|
||||
try {
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const newSettings = {
|
||||
...settings,
|
||||
...req.body,
|
||||
};
|
||||
$.write(newSettings, SETTINGS_KEY);
|
||||
await updateAvatar();
|
||||
await updateArtifactStore();
|
||||
success(res, newSettings);
|
||||
} catch (e) {
|
||||
$.error(`Failed to update settings: ${e.message ?? e}`);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
`FAILED_TO_UPDATE_SETTINGS`,
|
||||
`Failed to update settings`,
|
||||
`Reason: ${e.message ?? e}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateGitHubAvatar() {
|
||||
export async function updateAvatar() {
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const username = settings.githubUser;
|
||||
const { githubUser: username, syncPlatform } = settings;
|
||||
if (username) {
|
||||
try {
|
||||
const data = await $.http
|
||||
.get({
|
||||
url: `https://api.github.com/users/${username}`,
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
|
||||
},
|
||||
})
|
||||
.then((resp) => JSON.parse(resp.body));
|
||||
settings.avatarUrl = data['avatar_url'];
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Failed to fetch GitHub avatar for User: ${username}. Reason: ${
|
||||
err.message ?? err
|
||||
}`,
|
||||
);
|
||||
if (syncPlatform === 'gitlab') {
|
||||
try {
|
||||
const data = await $.http
|
||||
.get({
|
||||
url: `https://gitlab.com/api/v4/users?username=${encodeURIComponent(
|
||||
username,
|
||||
)}`,
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
|
||||
},
|
||||
})
|
||||
.then((resp) => JSON.parse(resp.body));
|
||||
settings.avatarUrl = data[0]['avatar_url'].replace(
|
||||
/(\?|&)s=\d+(&|$)/,
|
||||
'$1s=160$2',
|
||||
);
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Failed to fetch GitLab avatar for User: ${username}. Reason: ${
|
||||
err.message ?? err
|
||||
}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const data = await $.http
|
||||
.get({
|
||||
url: `https://api.github.com/users/${encodeURIComponent(
|
||||
username,
|
||||
)}`,
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
|
||||
},
|
||||
})
|
||||
.then((resp) => JSON.parse(resp.body));
|
||||
settings.avatarUrl = data['avatar_url'];
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Failed to fetch GitHub avatar for User: ${username}. Reason: ${
|
||||
err.message ?? err
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,19 +117,21 @@ export async function updateGitHubAvatar() {
|
||||
export async function updateArtifactStore() {
|
||||
$.log('Updating artifact store');
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const { gistToken } = settings;
|
||||
const { gistToken, syncPlatform } = settings;
|
||||
if (gistToken) {
|
||||
const manager = new Gist({
|
||||
token: gistToken,
|
||||
key: ARTIFACT_REPOSITORY_KEY,
|
||||
syncPlatform,
|
||||
});
|
||||
|
||||
try {
|
||||
const gist = await manager.locate();
|
||||
if (gist?.html_url) {
|
||||
$.log(`找到 Sub-Store Gist: ${gist.html_url}`);
|
||||
const url = gist?.html_url ?? gist?.web_url;
|
||||
if (url) {
|
||||
$.log(`找到 Sub-Store Gist: ${url}`);
|
||||
// 只需要保证 token 是对的, 现在 username 错误只会导致头像错误
|
||||
settings.artifactStore = gist.html_url;
|
||||
settings.artifactStore = url;
|
||||
settings.artifactStoreStatus = 'VALID';
|
||||
} else {
|
||||
$.error(`找不到 Sub-Store Gist`);
|
||||
|
||||
@@ -492,9 +492,18 @@ async function syncArtifacts() {
|
||||
if (artifact.sync) {
|
||||
artifact.updated = new Date().getTime();
|
||||
// extract real url from gist
|
||||
artifact.url = body.files[
|
||||
encodeURIComponent(artifact.name)
|
||||
]?.raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
|
||||
let files = body.files;
|
||||
let isGitLab;
|
||||
if (Array.isArray(files)) {
|
||||
isGitLab = true;
|
||||
files = Object.fromEntries(
|
||||
files.map((item) => [item.path, item]),
|
||||
);
|
||||
}
|
||||
const url = files[encodeURIComponent(artifact.name)]?.raw_url;
|
||||
artifact.url = isGitLab
|
||||
? url
|
||||
: url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -582,9 +591,16 @@ async function syncArtifact(req, res) {
|
||||
});
|
||||
artifact.updated = new Date().getTime();
|
||||
const body = JSON.parse(resp.body);
|
||||
artifact.url = body.files[
|
||||
encodeURIComponent(artifact.name)
|
||||
]?.raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
|
||||
let files = body.files;
|
||||
let isGitLab;
|
||||
if (Array.isArray(files)) {
|
||||
isGitLab = true;
|
||||
files = Object.fromEntries(files.map((item) => [item.path, item]));
|
||||
}
|
||||
const url = files[encodeURIComponent(artifact.name)]?.raw_url;
|
||||
artifact.url = isGitLab
|
||||
? url
|
||||
: url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
success(res, artifact);
|
||||
} catch (err) {
|
||||
|
||||
@@ -4,7 +4,12 @@ import { HTTP, ENV } from '@/vendor/open-api';
|
||||
import { hex_md5 } from '@/vendor/md5';
|
||||
import resourceCache from '@/utils/resource-cache';
|
||||
import headersResourceCache from '@/utils/headers-resource-cache';
|
||||
import { getFlowField } from '@/utils/flow';
|
||||
import {
|
||||
getFlowField,
|
||||
getFlowHeaders,
|
||||
parseFlowHeaders,
|
||||
validCheck,
|
||||
} from '@/utils/flow';
|
||||
import $ from '@/core/app';
|
||||
|
||||
const tasks = new Map();
|
||||
@@ -64,36 +69,40 @@ export default async function download(rawUrl, ua, timeout) {
|
||||
timeout: requestTimeout,
|
||||
});
|
||||
|
||||
const result = new Promise((resolve, reject) => {
|
||||
// try to find in app cache
|
||||
const cached = resourceCache.get(id);
|
||||
if (!$arguments?.noCache && cached) {
|
||||
resolve(cached);
|
||||
} else {
|
||||
$.info(
|
||||
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nURL: ${url}`,
|
||||
);
|
||||
http.get(url)
|
||||
.then((resp) => {
|
||||
const { body, headers } = resp;
|
||||
if (headers) {
|
||||
const flowInfo = getFlowField(headers);
|
||||
if (flowInfo) {
|
||||
headersResourceCache.set(url, flowInfo);
|
||||
}
|
||||
}
|
||||
if (body.replace(/\s/g, '').length === 0)
|
||||
reject(new Error('远程资源内容为空!'));
|
||||
else {
|
||||
resourceCache.set(id, body);
|
||||
resolve(body);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error(`无法下载 URL:${url}`));
|
||||
});
|
||||
let result;
|
||||
|
||||
// try to find in app cache
|
||||
const cached = resourceCache.get(id);
|
||||
if (!$arguments?.noCache && cached) {
|
||||
result = cached;
|
||||
} else {
|
||||
$.info(
|
||||
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nURL: ${url}`,
|
||||
);
|
||||
try {
|
||||
const { body, headers } = await http.get(url);
|
||||
|
||||
if (headers) {
|
||||
const flowInfo = getFlowField(headers);
|
||||
if (flowInfo) {
|
||||
headersResourceCache.set(url, flowInfo);
|
||||
}
|
||||
}
|
||||
if (body.replace(/\s/g, '').length === 0)
|
||||
throw new Error(new Error('远程资源内容为空'));
|
||||
|
||||
resourceCache.set(id, body);
|
||||
result = body;
|
||||
} catch (e) {
|
||||
throw new Error(`无法下载 URL ${url}: ${e.message ?? e}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 检查订阅有效性
|
||||
|
||||
if ($arguments?.validCheck) {
|
||||
await validCheck(parseFlowHeaders(await getFlowHeaders(url)));
|
||||
}
|
||||
|
||||
if (!isNode) {
|
||||
tasks.set(id, result);
|
||||
|
||||
@@ -88,17 +88,27 @@ export async function getFlowHeaders(rawUrl, ua, timeout) {
|
||||
export function parseFlowHeaders(flowHeaders) {
|
||||
if (!flowHeaders) return;
|
||||
// unit is KB
|
||||
const uploadMatch = flowHeaders.match(/upload=(-?)(\d+)/);
|
||||
const uploadMatch = flowHeaders.match(
|
||||
/upload=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
|
||||
);
|
||||
const upload = Number(uploadMatch[1] + uploadMatch[2]);
|
||||
|
||||
const downloadMatch = flowHeaders.match(/download=(-?)(\d+)/);
|
||||
const downloadMatch = flowHeaders.match(
|
||||
/download=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
|
||||
);
|
||||
const download = Number(downloadMatch[1] + downloadMatch[2]);
|
||||
|
||||
const total = Number(flowHeaders.match(/total=(\d+)/)[1]);
|
||||
const totalMatch = flowHeaders.match(
|
||||
/total=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
|
||||
);
|
||||
const total = Number(totalMatch[1] + totalMatch[2]);
|
||||
|
||||
// optional expire timestamp
|
||||
const match = flowHeaders.match(/expire=(\d+)/);
|
||||
const expires = match ? Number(match[1]) : undefined;
|
||||
const expireMatch = flowHeaders.match(
|
||||
/expire=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
|
||||
);
|
||||
const expires = expireMatch
|
||||
? Number(expireMatch[1] + expireMatch[2])
|
||||
: undefined;
|
||||
|
||||
return { expires, total, usage: { upload, download } };
|
||||
}
|
||||
@@ -110,3 +120,26 @@ export function flowTransfer(flow, unit = 'B') {
|
||||
? { value: flow.toFixed(1), unit: unit }
|
||||
: flowTransfer(flow / 1024, unitList[++unitIndex]);
|
||||
}
|
||||
|
||||
export function validCheck(flow) {
|
||||
if (!flow) {
|
||||
throw new Error('没有流量信息');
|
||||
}
|
||||
if (flow?.expires && flow.expires * 1000 < Date.now()) {
|
||||
const date = new Date(flow.expires * 1000).toLocaleDateString();
|
||||
throw new Error(`订阅已过期: ${date}`);
|
||||
}
|
||||
if (flow?.total) {
|
||||
const upload = flow.usage?.upload || 0;
|
||||
const download = flow.usage?.download || 0;
|
||||
if (flow.total - upload - download < 0) {
|
||||
const current = upload + download;
|
||||
const currT = flowTransfer(Math.abs(current));
|
||||
currT.value = current < 0 ? '-' + currT.value : currT.value;
|
||||
const totalT = flowTransfer(flow.total);
|
||||
throw new Error(
|
||||
`流量已用完: ${currT.value} ${currT.unit} / ${totalT.value} ${totalT.unit}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,64 +4,216 @@ import { HTTP } from '@/vendor/open-api';
|
||||
* Gist backup
|
||||
*/
|
||||
export default class Gist {
|
||||
constructor({ token, key }) {
|
||||
this.http = HTTP({
|
||||
baseURL: 'https://api.github.com',
|
||||
headers: {
|
||||
constructor({ token, key, syncPlatform }) {
|
||||
if (syncPlatform === 'gitlab') {
|
||||
this.headers = {
|
||||
'PRIVATE-TOKEN': `${token}`,
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
|
||||
};
|
||||
this.http = HTTP({
|
||||
baseURL: 'https://gitlab.com/api/v4',
|
||||
headers: { ...this.headers },
|
||||
events: {
|
||||
onResponse: (resp) => {
|
||||
if (/^[45]/.test(String(resp.statusCode))) {
|
||||
const body = JSON.parse(resp.body);
|
||||
return Promise.reject(
|
||||
`ERROR: ${body.message?.error ?? body.message}`,
|
||||
);
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.headers = {
|
||||
Authorization: `token ${token}`,
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
|
||||
},
|
||||
events: {
|
||||
onResponse: (resp) => {
|
||||
if (/^[45]/.test(String(resp.statusCode))) {
|
||||
return Promise.reject(
|
||||
`ERROR: ${JSON.parse(resp.body).message}`,
|
||||
);
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
};
|
||||
this.http = HTTP({
|
||||
baseURL: 'https://api.github.com',
|
||||
headers: { ...this.headers },
|
||||
events: {
|
||||
onResponse: (resp) => {
|
||||
if (/^[45]/.test(String(resp.statusCode))) {
|
||||
return Promise.reject(
|
||||
`ERROR: ${JSON.parse(resp.body).message}`,
|
||||
);
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.key = key;
|
||||
this.syncPlatform = syncPlatform;
|
||||
}
|
||||
|
||||
async locate() {
|
||||
return this.http.get('/gists').then((response) => {
|
||||
const gists = JSON.parse(response.body);
|
||||
for (let g of gists) {
|
||||
if (g.description === this.key) {
|
||||
return g;
|
||||
if (this.syncPlatform === 'gitlab') {
|
||||
return this.http.get('/snippets').then((response) => {
|
||||
const gists = JSON.parse(response.body);
|
||||
|
||||
for (let g of gists) {
|
||||
if (g.title === this.key) {
|
||||
return g;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
});
|
||||
return;
|
||||
});
|
||||
} else {
|
||||
return this.http.get('/gists').then((response) => {
|
||||
const gists = JSON.parse(response.body);
|
||||
for (let g of gists) {
|
||||
if (g.description === this.key) {
|
||||
return g;
|
||||
}
|
||||
}
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async upload(files) {
|
||||
if (Object.keys(files).length === 0) {
|
||||
async upload(input) {
|
||||
if (Object.keys(input).length === 0) {
|
||||
return Promise.reject('未提供需上传的文件');
|
||||
}
|
||||
|
||||
const gist = await this.locate();
|
||||
|
||||
let files = input;
|
||||
|
||||
if (gist?.id) {
|
||||
// update an existing gist
|
||||
return this.http.patch({
|
||||
url: `/gists/${gist.id}`,
|
||||
body: JSON.stringify({ files }),
|
||||
if (this.syncPlatform === 'gitlab') {
|
||||
gist.files = gist.files.reduce((acc, item) => {
|
||||
acc[item.path] = item;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
// console.log(`files`, files);
|
||||
// console.log(`gist`, gist.files);
|
||||
let actions = [];
|
||||
const result = { ...gist.files };
|
||||
Object.keys(files).map((key) => {
|
||||
if (result[key]) {
|
||||
if (
|
||||
files[key].content == null ||
|
||||
files[key].content === ''
|
||||
) {
|
||||
delete result[key];
|
||||
actions.push({
|
||||
action: 'delete',
|
||||
file_path: key,
|
||||
});
|
||||
} else {
|
||||
result[key] = files[key];
|
||||
actions.push({
|
||||
action: 'update',
|
||||
file_path: key,
|
||||
content: files[key].content,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
files[key].content == null ||
|
||||
files[key].content === ''
|
||||
) {
|
||||
delete result[key];
|
||||
delete files[key];
|
||||
} else {
|
||||
result[key] = files[key];
|
||||
actions.push({
|
||||
action: 'create',
|
||||
file_path: key,
|
||||
content: files[key].content,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log(`result`, result);
|
||||
console.log(`files`, files);
|
||||
console.log(`actions`, actions);
|
||||
|
||||
if (this.syncPlatform === 'gitlab') {
|
||||
if (Object.keys(result).length === 0) {
|
||||
return Promise.reject(
|
||||
'本次操作将导致所有文件的内容都为空, 无法更新 snippet',
|
||||
);
|
||||
}
|
||||
if (Object.keys(result).length > 10) {
|
||||
return Promise.reject(
|
||||
'本次操作将导致 snippet 的文件数超过 10, 无法更新 snippet',
|
||||
);
|
||||
}
|
||||
files = actions;
|
||||
return this.http.put({
|
||||
headers: {
|
||||
...this.headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
url: `/snippets/${gist.id}`,
|
||||
body: JSON.stringify({ files }),
|
||||
});
|
||||
} else {
|
||||
if (Object.keys(result).length === 0) {
|
||||
return Promise.reject(
|
||||
'本次操作将导致所有文件的内容都为空, 无法更新 gist',
|
||||
);
|
||||
}
|
||||
return this.http.patch({
|
||||
url: `/gists/${gist.id}`,
|
||||
body: JSON.stringify({ files }),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// create a new gist for backup
|
||||
return this.http.post({
|
||||
url: '/gists',
|
||||
body: JSON.stringify({
|
||||
description: this.key,
|
||||
public: false,
|
||||
files,
|
||||
}),
|
||||
});
|
||||
files = Object.entries(files).reduce((acc, [key, file]) => {
|
||||
if (file.content !== null && file.content !== '') {
|
||||
acc[key] = file;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
if (this.syncPlatform === 'gitlab') {
|
||||
if (Object.keys(files).length === 0) {
|
||||
return Promise.reject(
|
||||
'所有文件的内容都为空, 无法创建 snippet',
|
||||
);
|
||||
}
|
||||
files = Object.keys(files).map((key) => ({
|
||||
file_path: key,
|
||||
content: files[key].content,
|
||||
}));
|
||||
return this.http.post({
|
||||
headers: {
|
||||
...this.headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
url: '/snippets',
|
||||
body: JSON.stringify({
|
||||
title: this.key,
|
||||
visibility: 'private',
|
||||
files,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
if (Object.keys(files).length === 0) {
|
||||
return Promise.reject(
|
||||
'所有文件的内容都为空, 无法创建 gist',
|
||||
);
|
||||
}
|
||||
return this.http.post({
|
||||
url: '/gists',
|
||||
body: JSON.stringify({
|
||||
description: this.key,
|
||||
public: false,
|
||||
files,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
11
backend/src/vendor/open-api.js
vendored
11
backend/src/vendor/open-api.js
vendored
@@ -314,6 +314,17 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
|
||||
request[method.toLowerCase()](
|
||||
options,
|
||||
(err, response, body) => {
|
||||
// if (err) {
|
||||
// console.log(err);
|
||||
// } else {
|
||||
// console.log({
|
||||
// statusCode:
|
||||
// response.status || response.statusCode,
|
||||
// headers: response.headers,
|
||||
// body,
|
||||
// });
|
||||
// }
|
||||
|
||||
if (err) reject(err);
|
||||
else
|
||||
resolve({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!name=Sub-Store
|
||||
#!desc=高级订阅管理工具
|
||||
#!desc=高级订阅管理工具. 定时任务默认为每天 0 点
|
||||
#!openUrl=https://sub.store
|
||||
#!author=Peng-YM
|
||||
#!homepage=https://github.com/sub-store-org/Sub-Store
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name":"Sub-Store",
|
||||
"description":"",
|
||||
"task":[
|
||||
"0 0 * * * https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync, img-url=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png"
|
||||
]
|
||||
}
|
||||
"name": "Sub-Store",
|
||||
"description": "定时任务默认为每天 0 点",
|
||||
"task": [
|
||||
"0 0 * * * https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync, img-url=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png"
|
||||
]
|
||||
}
|
||||
@@ -12,6 +12,9 @@ Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
|
||||
安装使用 插件 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin) 即可。
|
||||
|
||||
### 2. Surge
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Sub-Store
|
||||
desc: 高级订阅管理工具 @Peng-YM
|
||||
desc: 高级订阅管理工具 @Peng-YM. 定时任务默认为每天 0 点
|
||||
|
||||
http:
|
||||
mitm:
|
||||
@@ -33,4 +33,4 @@ script-providers:
|
||||
|
||||
cron-sync-artifacts:
|
||||
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
|
||||
interval: 86400
|
||||
interval: 86400
|
||||
|
||||
15
config/Surge-Beta.sgmodule
Normal file
15
config/Surge-Beta.sgmodule
Normal file
@@ -0,0 +1,15 @@
|
||||
#!name=Sub-Store(β)
|
||||
#!desc=支持最新 Surge iOS TestFlight 版本的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 0 0 * * *
|
||||
#!category=订阅管理
|
||||
#!arguments=ability:http-client-policy,cronexp:0 0 * * *,sync:"Sub-Store Sync"
|
||||
#!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默认为每天 0 点\n\n3️⃣ sync\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务"
|
||||
|
||||
[MITM]
|
||||
hostname = %APPEND% sub.store
|
||||
|
||||
[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 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
|
||||
|
||||
{{{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
|
||||
@@ -1,5 +1,6 @@
|
||||
#!name=Sub-Store
|
||||
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数
|
||||
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 0 点
|
||||
#!category=订阅管理
|
||||
|
||||
[MITM]
|
||||
hostname = %APPEND% sub.store
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!name=Sub-Store
|
||||
#!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 可能会爆内存, 如果不需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用不带 ability 参数版本
|
||||
#!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 可能会爆内存, 如果不需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用不带 ability 参数版本. 定时任务默认为每天 0 点
|
||||
#!category=订阅管理
|
||||
|
||||
[MITM]
|
||||
hostname = %APPEND% sub.store
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!name=Sub-Store
|
||||
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数
|
||||
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 0 点
|
||||
#!category=订阅管理
|
||||
|
||||
[MITM]
|
||||
hostname = %APPEND% sub.store
|
||||
|
||||
Reference in New Issue
Block a user