Compare commits

..

25 Commits

Author SHA1 Message Date
xream
e229408a2d feat: 默认缓存阈值 1024KB 2024-11-24 12:31:18 +08:00
xream
514414587b feat: 默认超时 8000ms 2024-11-24 12:13:52 +08:00
egerndaddy
d4c419745e Update Egern.yaml 2024-11-22 23:59:59 +08:00
xream
fe3da254f4 feat: 支持 Egern 输出 2024-11-21 13:16:04 +08:00
xream
7d8132d7cd feat: 默认输出格式改为 V2Ray; accept 为 application/json 时, 输出 JSON; 响应增加 X-Powered-By Sub-Store 2024-11-19 23:06:45 +08:00
xream
bc1247efaf feat: 手动设置的订阅流量信息会附加到订阅自己的流量信息之前 2024-11-17 23:40:03 +08:00
xream
dea937df66 feat: 默认查询流量信息的 User-AgentQuantumult%20X/1.0.30 (iPhone14,2; iOS 15.6) 改为 clash; 流量信息缓存逻辑调整 2024-11-17 02:10:38 +08:00
xream
cfb5a8e082 feat: 支持解析订阅中的 profile-web-page-url 字段 2024-11-17 01:02:28 +08:00
xream
4790bf47d1 feat: Surge 密码解析支持首尾成对的单引号双引号, 输出时增加双引号 2024-11-16 21:50:52 +08:00
xream
56fd495fb6 feat: 支持更多的 subscription-userinfo 2024-11-12 22:20:46 +08:00
xream
f4639d9a34 feat: 支持更多的 subscription-userinfo 2024-11-12 22:06:22 +08:00
xream
cc58a5541e feat: 订阅刷新按钮逻辑调整为无缓存刷新订阅和流量 2024-11-10 01:22:48 +08:00
xream
772f431887 feat: 模块版文件中增加 token 路由 2024-11-08 18:10:39 +08:00
xream
2b60c515cd feat: 支持管理 token 2024-11-04 13:59:57 +08:00
xream
c8c22c3901 fix: 修复 VMess URI SNI 2024-11-01 20:27:23 +08:00
xream
d8f9466b84 feat(wip): 支持自定义 share token 2024-10-31 23:33:34 +08:00
xream
d12ccad382 feat: MMDB 加入 $utils.ipasn 2024-10-31 01:39:13 +08:00
xream
b4358663cc feat(wip): 支持 JWT 2024-10-31 00:23:45 +08:00
xream
aba6264988 feat(wip): 支持 JWT 2024-10-30 23:08:01 +08:00
xream
2320ab3838 feat(wip): 支持 JWT 2024-10-30 22:51:31 +08:00
xream
542957d34a feat(wip): 支持 JWT 2024-10-30 22:27:39 +08:00
xream
07e50175f9 feat: cipher 应为小写 2024-10-30 16:07:27 +08:00
xream
e09d66060d feat: 远程订阅支持 insecure 不验证服务器证书 2024-10-30 14:33:34 +08:00
xream
b048ecdfff fix: 修复 surge mac 未开启 mihomo 时, 对于不支持的节点未报错, 导致出现 proxy 为 undefined 的问题 2024-10-29 18:31:02 +08:00
xream
aac72fb9a3 feat: Surge 支持 udp-port, 修复 udp-relay 参数解析 2024-10-27 19:00:42 +08:00
31 changed files with 8049 additions and 7550 deletions

View File

@@ -7,7 +7,7 @@
</div>
<p align="center" color="#6a737d">
Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.
Advanced Subscription Manager for QX, Loon, Surge, Stash, Egern and Shadowrocket.
</p>
[![Build](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/sub-store-org/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/sub-store-org/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/sub-store-org/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/sub-store-org/Sub-Store)
@@ -49,6 +49,7 @@ Core functionalities:
- [x] Surge
- [x] SurgeMac(Use mihomo to support protocols that are not supported by Surge itself)
- [x] Loon
- [x] Egern
- [x] Shadowrocket
- [x] QX
- [x] sing-box

View File

@@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.14.403",
"version": "2.14.427",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js",
"scripts": {
@@ -30,11 +30,10 @@
"js-base64": "^3.7.2",
"jsrsasign": "^11.1.0",
"lodash": "^4.17.21",
"ms": "^2.1.3",
"nanoid": "^3.3.3",
"request": "^2.88.2",
"requests": "^0.3.0",
"semver": "^7.3.7",
"static-js-yaml": "^1.0.0",
"uuid": "^8.3.2"
"static-js-yaml": "^1.0.0"
},
"devDependencies": {
"@babel/core": "^7.18.0",

14661
backend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ export const FILES_KEY = 'files';
export const MODULES_KEY = 'modules';
export const ARTIFACTS_KEY = 'artifacts';
export const RULES_KEY = 'rules';
export const TOKENS_KEY = 'tokens';
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';

View File

@@ -327,6 +327,9 @@ function formatTransportPath(path) {
}
function lastParse(proxy) {
if (typeof proxy.cipher === 'string') {
proxy.cipher = proxy.cipher.toLowerCase();
}
if (typeof proxy.password === 'number') {
proxy.password = numberToString(proxy.password);
}

View File

@@ -316,7 +316,7 @@ function URI_VMess() {
);
}
// https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
if (proxy.tls && proxy.sni) {
if (proxy.tls && params.sni && params.sni !== '') {
proxy.sni = params.sni;
}
let httpupgrade = false;
@@ -340,6 +340,10 @@ function URI_VMess() {
} else if (params.net === 'h2' || proxy.network === 'h2') {
proxy.network = 'h2';
}
// 暂不支持 tcp + host + path
// else if (params.net === 'tcp' || proxy.network === 'tcp') {
// proxy.network = 'tcp';
// }
if (proxy.network) {
let transportHost = params.host ?? params.obfsParam;
try {

View File

@@ -41,7 +41,7 @@ start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v
return proxy;
}
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/udp_port/others)* {
proxy.type = "ss";
// handle obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -193,7 +193,7 @@ snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); }
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
@@ -219,7 +219,7 @@ obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; };
obfs_uri = comma "obfs-uri" equals path:uri { obfs.path = path }
uri = $[^,]+
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
@@ -240,6 +240,7 @@ idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"]
private_key = comma "private-key" equals match:[^,]+ { proxy["keystore-private-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }

View File

@@ -39,7 +39,7 @@ start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v
return proxy;
}
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/udp_port/others)* {
proxy.type = "ss";
// handle obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -191,7 +191,7 @@ snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); }
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
@@ -217,7 +217,7 @@ obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; };
obfs_uri = comma "obfs-uri" equals path:uri { obfs.path = path }
uri = $[^,]+
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
@@ -238,6 +238,7 @@ idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"]
private_key = comma "private-key" equals match:[^,]+ { proxy["keystore-private-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }

View File

@@ -561,7 +561,7 @@ function ResolveDomainOperator({
throw new Error(`域名解析服务提供方 ${provider} 不支持 ${_type}`);
}
const { defaultTimeout } = $.read(SETTINGS_KEY);
const requestTimeout = timeout || defaultTimeout;
const requestTimeout = timeout || defaultTimeout || 8000;
let type = ['IPv6', 'IP4P'].includes(_type) ? 'IPv6' : 'IPv4';
const resolver = DOMAIN_RESOLVERS[provider];

View File

@@ -0,0 +1,298 @@
export default function Egern_Producer() {
const type = 'ALL';
const produce = (proxies, type, opts = {}) => {
// https://egernapp.com/zh-CN/docs/configuration/proxies
const list = proxies
.filter((proxy) => {
if (opts['include-unsupported-proxy']) return true;
if (
![
'http',
'socks5',
'ss',
'trojan',
'hysteria2',
'vless',
'vmess',
].includes(proxy.type) ||
(proxy.type === 'ss' &&
((proxy.plugin === 'obfs' &&
!['http', 'tls'].includes(
proxy['plugin-opts']?.mode,
)) ||
![
'chacha20-ietf-poly1305',
'chacha20-poly1305',
'aes-256-gcm',
'aes-128-gcm',
'none',
'tbale',
'rc4',
'rc4-md5',
'aes-128-cfb',
'aes-192-cfb',
'aes-256-cfb',
'aes-128-ctr',
'aes-192-ctr',
'aes-256-ctr',
'bf-cfb',
'camellia-128-cfb',
'camellia-192-cfb',
'camellia-256-cfb',
'cast5-cfb',
'des-cfb',
'idea-cfb',
'rc2-cfb',
'seed-cfb',
'salsa20',
'chacha20',
'chacha20-ietf',
].includes(proxy.cipher))) ||
(proxy.type === 'vmess' &&
(![
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
'zero',
].includes(proxy.cipher) ||
(!['http', 'ws', 'tcp'].includes(proxy.network) &&
proxy.network))) ||
(proxy.type === 'trojan' &&
!['http', 'ws', 'tcp'].includes(proxy.network) &&
proxy.network) ||
(proxy.type === 'vless' &&
(typeof proxy.flow !== 'undefined' ||
proxy['reality-opts'] ||
(!['http', 'ws', 'tcp'].includes(proxy.network) &&
proxy.network)))
) {
return false;
}
return true;
})
.map((proxy) => {
if (proxy.type === 'http') {
proxy = {
type: 'http',
name: proxy.name,
server: proxy.server,
port: proxy.port,
username: proxy.username,
password: proxy.password,
tfo: proxy.tfo || proxy['fast-open'],
next_hop: proxy.next_hop,
};
} else if (proxy.type === 'socks5') {
proxy = {
type: 'socks5',
name: proxy.name,
server: proxy.server,
port: proxy.port,
username: proxy.username,
password: proxy.password,
tfo: proxy.tfo || proxy['fast-open'],
udp_relay:
proxy.udp || proxy.udp_relay || proxy.udp_relay,
next_hop: proxy.next_hop,
};
} else if (proxy.type === 'ss') {
proxy = {
type: 'shadowsocks',
name: proxy.name,
method:
proxy.cipher === 'chacha20-ietf-poly1305'
? 'chacha20-poly1305'
: proxy.cipher,
server: proxy.server,
port: proxy.port,
password: proxy.password,
tfo: proxy.tfo || proxy['fast-open'],
udp_relay:
proxy.udp || proxy.udp_relay || proxy.udp_relay,
next_hop: proxy.next_hop,
};
if (proxy.plugin === 'obfs') {
proxy.obfs = proxy['plugin-opts'].mode;
proxy.obfs_host = proxy['plugin-opts'].host;
proxy.obfs_uri = proxy['plugin-opts'].path;
}
} else if (proxy.type === 'hysteria2') {
proxy = {
type: 'hysteria2',
name: proxy.name,
server: proxy.server,
port: proxy.port,
auth: proxy.password,
tfo: proxy.tfo || proxy['fast-open'],
udp_relay:
proxy.udp || proxy.udp_relay || proxy.udp_relay,
next_hop: proxy.next_hop,
sni: proxy.sni,
skip_tls_verify: proxy['skip-cert-verify'],
};
if (proxy['obfs-password'] && proxy.obfs == 'salamander') {
proxy.obfs = 'salamander';
proxy.obfs_password = proxy['obfs-password'];
}
} else if (proxy.type === 'trojan') {
if (proxy.network === 'ws') {
proxy.websocket = {
path: proxy['ws-opts']?.path,
host: proxy['ws-opts']?.headers?.Host,
};
}
proxy = {
type: 'trojan',
name: proxy.name,
server: proxy.server,
port: proxy.port,
password: proxy.password,
tfo: proxy.tfo || proxy['fast-open'],
udp_relay:
proxy.udp || proxy.udp_relay || proxy.udp_relay,
next_hop: proxy.next_hop,
sni: proxy.sni,
skip_tls_verify: proxy['skip-cert-verify'],
websocket: proxy.websocket,
};
} else if (proxy.type === 'vmess') {
if (proxy.network === 'ws') {
proxy.transport = {
[proxy.tls ? 'wss' : 'ws']: {
path: proxy['ws-opts']?.path,
headers: {
Host: proxy['ws-opts']?.headers?.Host,
},
sni: proxy.tls ? proxy.sni : undefined,
skip_tls_verify: proxy.tls
? proxy['skip-cert-verify']
: undefined,
},
};
} else if (proxy.network === 'http') {
proxy.transport = {
http: {
method: proxy['http-opts']?.method,
path: proxy['http-opts']?.path,
headers: {
Host: Array.isArray(
proxy['http-opts']?.headers?.Host,
)
? proxy['http-opts']?.headers?.Host[0]
: proxy['http-opts']?.headers?.Host,
},
skip_tls_verify: proxy['skip-cert-verify'],
},
};
} else if (proxy.network === 'tcp' || !proxy.network) {
proxy.transport = {
[proxy.tls ? 'tls' : 'tcp']: {
sni: proxy.tls ? proxy.sni : undefined,
skip_tls_verify: proxy.tls
? proxy['skip-cert-verify']
: undefined,
},
};
}
proxy = {
type: 'vmess',
name: proxy.name,
server: proxy.server,
port: proxy.port,
user_id: proxy.uuid,
security: proxy.cipher,
tfo: proxy.tfo || proxy['fast-open'],
legacy: proxy.legacy,
udp_relay:
proxy.udp || proxy.udp_relay || proxy.udp_relay,
next_hop: proxy.next_hop,
transport: proxy.transport,
// sni: proxy.sni,
// skip_tls_verify: proxy['skip-cert-verify'],
};
} else if (proxy.type === 'vless') {
if (proxy.network === 'ws') {
proxy.transport = {
[proxy.tls ? 'wss' : 'ws']: {
path: proxy['ws-opts']?.path,
headers: {
Host: proxy['ws-opts']?.headers?.Host,
},
sni: proxy.tls ? proxy.sni : undefined,
skip_tls_verify: proxy.tls
? proxy['skip-cert-verify']
: undefined,
},
};
} else if (proxy.network === 'http') {
proxy.transport = {
http: {
method: proxy['http-opts']?.method,
path: proxy['http-opts']?.path,
headers: {
Host: Array.isArray(
proxy['http-opts']?.headers?.Host,
)
? proxy['http-opts']?.headers?.Host[0]
: proxy['http-opts']?.headers?.Host,
},
skip_tls_verify: proxy['skip-cert-verify'],
},
};
} else if (proxy.network === 'tcp' || !proxy.network) {
proxy.transport = {
[proxy.tls ? 'tls' : 'tcp']: {
sni: proxy.tls ? proxy.sni : undefined,
skip_tls_verify: proxy.tls
? proxy['skip-cert-verify']
: undefined,
},
};
}
proxy = {
type: 'vless',
name: proxy.name,
server: proxy.server,
port: proxy.port,
user_id: proxy.uuid,
security: proxy.cipher,
tfo: proxy.tfo || proxy['fast-open'],
legacy: proxy.legacy,
udp_relay:
proxy.udp || proxy.udp_relay || proxy.udp_relay,
next_hop: proxy.next_hop,
transport: proxy.transport,
// sni: proxy.sni,
// skip_tls_verify: proxy['skip-cert-verify'],
};
}
delete proxy.subName;
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
delete proxy['no-resolve'];
if (type !== 'internal') {
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
}
}
return {
[proxy.type]: {
...proxy,
type: undefined,
},
};
});
return type === 'internal'
? list
: 'proxies:\n' +
list
.map((proxy) => ' - ' + JSON.stringify(proxy) + '\n')
.join('');
};
return { type, produce };
}

View File

@@ -10,6 +10,7 @@ import QX_Producer from './qx';
import Shadowrocket_Producer from './shadowrocket';
import Surfboard_Producer from './surfboard';
import singbox_Producer from './sing-box';
import Egern_Producer from './egern';
function JSON_Producer() {
const type = 'ALL';
@@ -34,4 +35,5 @@ export default {
ShadowRocket: Shadowrocket_Producer(),
Surfboard: Surfboard_Producer(),
'sing-box': singbox_Producer(),
Egern: Egern_Producer(),
};

View File

@@ -93,7 +93,7 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
throw new Error(`cipher ${proxy.cipher} is not supported`);
}
result.append(`,encrypt-method=${proxy.cipher}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -125,6 +125,8 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// udp-port
result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
@@ -191,7 +193,7 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
function trojan(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -364,7 +366,7 @@ function ssh(proxy) {
result.append(`${proxy.name}=ssh,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username');
// 所有的类似的字段都有双引号的问题 暂不处理
result.appendIfPresent(`,${proxy.password}`, 'password');
result.appendIfPresent(`,"${proxy.password}"`, 'password');
// https://manual.nssurge.com/policy/ssh.html
// 需配合 Keystore
@@ -429,7 +431,7 @@ function http(proxy) {
const type = proxy.tls ? 'https' : 'http';
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,${proxy.password}`, 'password');
result.appendIfPresent(`,"${proxy.password}"`, 'password');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -507,7 +509,7 @@ function socks5(proxy) {
const type = proxy.tls ? 'socks5-tls' : 'socks5';
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,${proxy.password}`, 'password');
result.appendIfPresent(`,"${proxy.password}"`, 'password');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -673,7 +675,7 @@ function tuic(proxy) {
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,uuid=${proxy.uuid}`, 'uuid');
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
result.appendIfPresent(`,token=${proxy.token}`, 'token');
result.appendIfPresent(
@@ -948,7 +950,7 @@ function hysteria2(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=hysteria2,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
if (isPresent(proxy, 'ports')) {
result.append(`,port-hopping="${proxy.ports.replace(/,/g, ';')}"`);

View File

@@ -25,6 +25,10 @@ export default function SurgeMac_Producer() {
`${proxy.name} is not supported on ${targetPlatform}, try to use Mihomo(SurgeMac - External Proxy Program) instead`,
);
return mihomo(proxy, type, opts);
} else {
throw new Error(
`Surge for macOS 可手动指定链接参数 target=SurgeMac 或在 同步配置 中指定 SurgeMac 来启用 mihomo 支援 Surge 本身不支持的协议`,
);
}
}
}

View File

@@ -21,6 +21,7 @@ import registerSettingRoutes from '@/restful/settings';
import registerMiscRoutes from '@/restful/miscs';
import registerSortRoutes from '@/restful/sort';
import registerFileRoutes from '@/restful/file';
import registerTokenRoutes from '@/restful/token';
import registerModuleRoutes from '@/restful/module';
migrate();
@@ -32,6 +33,7 @@ function serve() {
// register routes
registerCollectionRoutes($app);
registerSubscriptionRoutes($app);
registerTokenRoutes($app);
registerFileRoutes($app);
registerModuleRoutes($app);
registerArtifactRoutes($app);

View File

@@ -13,6 +13,9 @@ import { getISO } from '@/utils/geo';
import env from '@/utils/env';
export default function register($app) {
$app.get('/share/col/:name', downloadCollection);
$app.get('/share/sub/:name', downloadSubscription);
$app.get('/download/collection/:name', downloadCollection);
$app.get('/download/:name', downloadSubscription);
$app.get(
@@ -72,6 +75,7 @@ async function downloadSubscription(req, res) {
includeUnsupportedProxy,
resultFormat,
proxy,
noCache,
} = req.query;
let $options = {};
if (req.query.$options) {
@@ -128,6 +132,10 @@ async function downloadSubscription(req, res) {
$.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);
}
if (noCache) {
$.info(`指定不使用缓存: ${noCache}`);
}
const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
if (sub) {
@@ -148,8 +156,9 @@ async function downloadSubscription(req, res) {
},
$options,
proxy,
noCache,
});
let flowInfo;
if (
sub.source !== 'local' ||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
@@ -184,8 +193,8 @@ async function downloadSubscription(req, res) {
}
if (!$arguments.noFlow) {
// forward flow headers
const flowInfo = await getFlowHeaders(
url,
flowInfo = await getFlowHeaders(
$arguments?.insecure ? `${url}#insecure` : url,
$arguments.flowUserAgent,
undefined,
proxy || sub.proxy,
@@ -204,7 +213,10 @@ async function downloadSubscription(req, res) {
}
}
if (sub.subUserinfo) {
res.set('subscription-userinfo', sub.subUserinfo);
res.set(
'subscription-userinfo',
[sub.subUserinfo, flowInfo].filter((i) => i).join('; '),
);
}
if (platform === 'JSON') {
@@ -280,6 +292,7 @@ async function downloadCollection(req, res) {
includeUnsupportedProxy,
resultFormat,
proxy,
noCache,
} = req.query;
let $options = {};
@@ -322,6 +335,9 @@ async function downloadCollection(req, res) {
if (useMihomoExternal) {
$.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);
}
if (noCache) {
$.info(`指定不使用缓存: ${noCache}`);
}
if (collection) {
try {
@@ -337,6 +353,7 @@ async function downloadCollection(req, res) {
},
$options,
proxy,
noCache,
});
// forward flow header from the first subscription in this collection
@@ -344,6 +361,7 @@ async function downloadCollection(req, res) {
const subnames = collection.subscriptions;
if (subnames.length > 0) {
const sub = findByName(allSubs, subnames[0]);
let flowInfo;
if (
sub.source !== 'local' ||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
@@ -377,8 +395,8 @@ async function downloadCollection(req, res) {
}
}
if (!$arguments.noFlow) {
const flowInfo = await getFlowHeaders(
url,
flowInfo = await getFlowHeaders(
$arguments?.insecure ? `${url}#insecure` : url,
$arguments.flowUserAgent,
undefined,
proxy || sub.proxy || collection.proxy,
@@ -397,7 +415,10 @@ async function downloadCollection(req, res) {
}
}
if (sub.subUserinfo) {
res.set('subscription-userinfo', sub.subUserinfo);
res.set(
'subscription-userinfo',
[sub.subUserinfo, flowInfo].filter((i) => i).join('; '),
);
}
}

View File

@@ -13,6 +13,8 @@ import { produceArtifact } from '@/restful/sync';
export default function register($app) {
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
$app.get('/share/file/:name', getFile);
$app.route('/api/file/:name')
.get(getFile)
.patch(updateFile)

View File

@@ -4,11 +4,13 @@ import migrate from '@/utils/migration';
import download from '@/utils/download';
import { syncArtifacts } from '@/restful/sync';
import { gistBackupAction } from '@/restful/miscs';
import { TOKENS_KEY } from '@/constants';
import registerSubscriptionRoutes from './subscriptions';
import registerCollectionRoutes from './collections';
import registerArtifactRoutes from './artifacts';
import registerFileRoutes from './file';
import registerTokenRoutes from './token';
import registerModuleRoutes from './module';
import registerSyncRoutes from './sync';
import registerDownloadRoutes from './download';
@@ -36,6 +38,7 @@ export default function serve() {
registerSettingRoutes($app);
registerArtifactRoutes($app);
registerFileRoutes($app);
registerTokenRoutes($app);
registerModuleRoutes($app);
registerSyncRoutes($app);
registerNodeInfoRoutes($app);
@@ -143,7 +146,7 @@ export default function serve() {
try {
fs.accessSync(path.join(fe_abs_path, 'index.html'));
} catch (e) {
throw new Error(
$.error(
`[FRONTEND] index.html file not found in ${fe_abs_path}`,
);
}
@@ -158,6 +161,7 @@ export default function serve() {
const staticFileMiddleware = express_.static(fe_path);
let be_share_rewrite = '/share/:type/:name';
let be_api_rewrite = '';
let be_download_rewrite = '';
let be_api = '/api/';
@@ -174,15 +178,39 @@ export default function serve() {
be_download_rewrite = `${
fe_be_path === '/' ? '' : fe_be_path
}${be_download}`;
app.use(
be_share_rewrite,
createProxyMiddleware({
target: `http://127.0.0.1:${port}`,
changeOrigin: true,
pathRewrite: (path, req) => {
if (req.method.toLowerCase() !== 'get')
throw new Error('Method not allowed');
const tokens = $.read(TOKENS_KEY) || [];
const token = tokens.find(
(t) =>
t.token === req.query.token &&
t.type === req.params.type &&
t.name === req.params.name &&
(t.exp == null || t.exp > Date.now()),
);
if (!token) throw new Error('Forbbiden');
return path;
},
}),
);
app.use(
be_api_rewrite,
createProxyMiddleware({
target: `http://127.0.0.1:${port}`,
changeOrigin: true,
pathRewrite: (path) => {
return path.startsWith(be_api_rewrite)
const newPath = path.startsWith(be_api_rewrite)
? path.replace(be_api_rewrite, be_api)
: path;
return newPath.includes('?')
? `${newPath}&share=true`
: `${newPath}?share=true`;
},
}),
);
@@ -220,6 +248,9 @@ export default function serve() {
$.info(
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_download_rewrite} -> http://127.0.0.1:${port}${be_download}`,
);
$.info(
`[SHARE BACKEND] ${fe_address}:${fe_port}${be_share_rewrite}`,
);
}
});
}

View File

@@ -65,6 +65,9 @@ export default function register($app) {
}
function getEnv(req, res) {
if (req.query.share) {
env.feature.share = true;
}
success(res, env);
}

View File

@@ -125,58 +125,53 @@ async function getFlowInfo(req, res) {
);
return;
}
if (sub.subUserinfo) {
try {
success(res, {
...parseFlowHeaders(sub.subUserinfo),
remainingDays: getRmainingDays({
resetDay: $arguments.resetDay,
startDate: $arguments.startDate,
cycleDays: $arguments.cycleDays,
}),
});
} catch (e) {
$.error(
`Failed to parse flow info for local subscription ${name}: ${
e.message ?? e
}`,
);
failed(
res,
new RequestInvalidError(
'NO_FLOW_INFO',
'N/A',
`Failed to parse flow info`,
),
);
}
} else {
const flowHeaders = await getFlowHeaders(
url,
$arguments.flowUserAgent,
undefined,
sub.proxy,
$arguments.flowUrl,
const flowHeaders = await getFlowHeaders(
$arguments?.insecure ? `${url}#insecure` : url,
$arguments.flowUserAgent,
undefined,
sub.proxy,
$arguments.flowUrl,
);
if (!flowHeaders && !sub.subUserinfo) {
failed(
res,
new InternalServerError(
'NO_FLOW_INFO',
'No flow info',
`Failed to fetch flow headers`,
),
);
if (!flowHeaders) {
failed(
res,
new InternalServerError(
'NO_FLOW_INFO',
'No flow info',
`Failed to fetch flow headers`,
),
);
return;
}
success(res, {
...parseFlowHeaders(flowHeaders),
remainingDays: getRmainingDays({
resetDay: $arguments.resetDay,
startDate: $arguments.startDate,
cycleDays: $arguments.cycleDays,
}),
return;
}
try {
const remainingDays = getRmainingDays({
resetDay: $arguments.resetDay,
startDate: $arguments.startDate,
cycleDays: $arguments.cycleDays,
});
const result = {
...parseFlowHeaders(
[sub.subUserinfo, flowHeaders].filter((i) => i).join('; '),
),
};
if (remainingDays != null) {
result.remainingDays = remainingDays;
}
success(res, result);
} catch (e) {
$.error(
`Failed to parse flow info for local subscription ${name}: ${
e.message ?? e
}`,
);
failed(
res,
new RequestInvalidError(
'NO_FLOW_INFO',
'N/A',
`Failed to parse flow info`,
),
);
}
} catch (err) {
failed(

View File

@@ -39,6 +39,7 @@ async function produceArtifact({
awaitCustomCache,
$options,
proxy,
noCache,
}) {
platform = platform || 'JSON';
@@ -72,6 +73,7 @@ async function produceArtifact({
proxy || sub.proxy,
undefined,
awaitCustomCache,
noCache,
);
} catch (err) {
errors[url] = err;
@@ -119,6 +121,7 @@ async function produceArtifact({
proxy || sub.proxy,
undefined,
awaitCustomCache,
noCache,
);
} catch (err) {
errors[url] = err;
@@ -237,6 +240,9 @@ async function produceArtifact({
proxy ||
sub.proxy ||
collection.proxy,
undefined,
undefined,
noCache,
);
} catch (err) {
errors[url] = err;
@@ -410,6 +416,9 @@ async function produceArtifact({
ua || file.ua,
undefined,
file.proxy || proxy,
undefined,
undefined,
noCache,
);
} catch (err) {
errors[url] = err;
@@ -458,6 +467,9 @@ async function produceArtifact({
ua || file.ua,
undefined,
file.proxy || proxy,
undefined,
undefined,
noCache,
);
} catch (err) {
errors[url] = err;

View File

@@ -0,0 +1,181 @@
import { deleteByName } from '@/utils/database';
import { ENV } from '@/vendor/open-api';
import { TOKENS_KEY, SUBS_KEY, FILES_KEY, COLLECTIONS_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
import { RequestInvalidError, InternalServerError } from '@/restful/errors';
export default function register($app) {
if (!$.read(TOKENS_KEY)) $.write([], TOKENS_KEY);
$app.post('/api/token', signToken);
$app.route('/api/token/:token').delete(deleteToken);
$app.route('/api/tokens').get(getAllTokens);
}
function deleteToken(req, res) {
let { token } = req.params;
token = decodeURIComponent(token);
$.info(`正在删除:${token}`);
let allTokens = $.read(TOKENS_KEY);
deleteByName(allTokens, token, 'token');
$.write(allTokens, TOKENS_KEY);
success(res);
}
function getAllTokens(req, res) {
const { type, name } = req.query;
const allTokens = $.read(TOKENS_KEY) || [];
success(
res,
type || name
? allTokens.filter(
(item) =>
(type ? item.type === type : true) &&
(name ? item.name === name : true),
)
: allTokens,
);
}
async function signToken(req, res) {
if (!ENV().isNode) {
return failed(
res,
new RequestInvalidError(
'INVALID_ENV',
`This endpoint is only available in Node.js environment`,
),
);
}
try {
const { payload, options } = req.body;
const ms = eval(`require("ms")`);
let token = payload?.token;
if (token != null) {
if (typeof token !== 'string' || token.length < 1) {
return failed(
res,
new RequestInvalidError(
'INVALID_CUSTOM_TOKEN',
`Invalid custom token: ${token}`,
),
);
}
const tokens = $.read(TOKENS_KEY) || [];
if (tokens.find((t) => t.token === token)) {
return failed(
res,
new RequestInvalidError(
'DUPLICATE_TOKEN',
`Token ${token} already exists`,
),
);
}
}
const type = payload?.type;
const name = payload?.name;
if (!type || !name)
return failed(
res,
new RequestInvalidError(
'INVALID_PAYLOAD',
`payload type and name are required`,
),
);
if (type === 'col') {
const collections = $.read(COLLECTIONS_KEY) || [];
const collection = collections.find((c) => c.name === name);
if (!collection)
return failed(
res,
new RequestInvalidError(
'INVALID_COLLECTION',
`collection ${name} not found`,
),
);
} else if (type === 'file') {
const files = $.read(FILES_KEY) || [];
const file = files.find((f) => f.name === name);
if (!file)
return failed(
res,
new RequestInvalidError(
'INVALID_FILE',
`file ${name} not found`,
),
);
} else if (type === 'sub') {
const subs = $.read(SUBS_KEY) || [];
const sub = subs.find((s) => s.name === name);
if (!sub)
return failed(
res,
new RequestInvalidError(
'INVALID_SUB',
`sub ${name} not found`,
),
);
} else {
return failed(
res,
new RequestInvalidError(
'INVALID_TYPE',
`type ${name} not supported`,
),
);
}
let expiresIn = options?.expiresIn;
if (options?.expiresIn != null) {
expiresIn = ms(options.expiresIn);
if (expiresIn == null || isNaN(expiresIn) || expiresIn <= 0) {
return failed(
res,
new RequestInvalidError(
'INVALID_EXPIRES_IN',
`Invalid expiresIn option: ${options.expiresIn}`,
),
);
}
}
// const secret = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
const nanoid = eval(`require("nanoid")`);
const tokens = $.read(TOKENS_KEY) || [];
// const now = Date.now();
// for (const key in tokens) {
// const token = tokens[key];
// if (token.exp != null || token.exp < now) {
// delete tokens[key];
// }
// }
if (!token) {
do {
token = nanoid.customAlphabet(nanoid.urlAlphabet)();
} while (tokens.find((t) => t.token === token));
}
tokens.push({
...payload,
token,
createdAt: Date.now(),
expiresIn: expiresIn > 0 ? options?.expiresIn : undefined,
exp: expiresIn > 0 ? Date.now() + expiresIn : undefined,
});
$.write(tokens, TOKENS_KEY);
return success(res, {
token,
// secret,
});
} catch (e) {
return failed(
res,
new InternalServerError(
'TOKEN_SIGN_FAILED',
`Failed to sign token`,
`Reason: ${e.message ?? e}`,
),
);
}
}

View File

@@ -1,17 +1,17 @@
export function findByName(list, name) {
return list.find((item) => item.name === name);
export function findByName(list, name, field = 'name') {
return list.find((item) => item[field] === name);
}
export function findIndexByName(list, name) {
return list.findIndex((item) => item.name === name);
export function findIndexByName(list, name, field = 'name') {
return list.findIndex((item) => item[field] === name);
}
export function deleteByName(list, name) {
const idx = findIndexByName(list, name);
export function deleteByName(list, name, field = 'name') {
const idx = findIndexByName(list, name, field);
list.splice(idx, 1);
}
export function updateByName(list, name, newItem) {
const idx = findIndexByName(list, name);
export function updateByName(list, name, newItem, field = 'name') {
const idx = findIndexByName(list, name, field);
list[idx] = newItem;
}

View File

@@ -21,6 +21,7 @@ export default async function download(
customProxy,
skipCustomCache,
awaitCustomCache,
noCache,
) {
let $arguments = {};
let url = rawUrl.replace(/#noFlow$/, '');
@@ -43,14 +44,19 @@ export default async function download(
}
}
const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();
const { defaultProxy, defaultUserAgent, defaultTimeout, cacheThreshold } =
$.read(SETTINGS_KEY);
const {
defaultProxy,
defaultUserAgent,
defaultTimeout,
cacheThreshold: defaultCacheThreshold,
} = $.read(SETTINGS_KEY);
const cacheThreshold = defaultCacheThreshold || 1024;
let proxy = customProxy || defaultProxy;
if ($.env.isNode) {
proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
}
const userAgent = ua || defaultUserAgent || 'clash.meta';
const requestTimeout = timeout || defaultTimeout;
const requestTimeout = timeout || defaultTimeout || 8000;
const id = hex_md5(userAgent + url);
if ($arguments?.cacheKey === true) {
@@ -65,7 +71,7 @@ export default async function download(
if (customCacheKey && !skipCustomCache) {
const customCached = $.read(customCacheKey);
const cached = resourceCache.get(id);
if (!$arguments?.noCache && cached) {
if (!noCache && !$arguments?.noCache && cached) {
$.info(
`乐观缓存: URL ${url}\n存在有效的常规缓存\n使用常规缓存以避免重复请求`,
);
@@ -149,16 +155,21 @@ export default async function download(
// try to find in app cache
const cached = resourceCache.get(id);
if (!$arguments?.noCache && cached) {
$.info(`使用缓存: ${url}`);
if (!noCache && !$arguments?.noCache && cached) {
$.info(`使用缓存: ${url}, ${userAgent}`);
result = cached;
if (customCacheKey) {
$.info(`URL ${url}\n写入自定义缓存 ${$arguments?.cacheKey}`);
$.write(cached, customCacheKey);
}
} else {
const insecure = $arguments?.insecure
? isNode
? { strictSSL: false }
: { insecure: true }
: undefined;
$.info(
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nURL: ${url}`,
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nInsecure: ${!!insecure}\nURL: ${url}`,
);
try {
const { body, headers } = await http.get({
@@ -167,12 +178,13 @@ export default async function download(
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
...(insecure ? insecure : {}),
});
if (headers) {
const flowInfo = getFlowField(headers);
if (flowInfo) {
headersResourceCache.set(url, flowInfo);
headersResourceCache.set(id, flowInfo);
}
}
if (body.replace(/\s/g, '').length === 0)

View File

@@ -24,6 +24,7 @@ if (isLanceX) backend = 'LanceX';
if (isGUIforCores) backend = 'GUI.for.Cores';
let meta = {};
let feature = {};
try {
if (typeof $environment !== 'undefined') {
@@ -63,5 +64,6 @@ try {
export default {
backend,
version: substoreVersion,
feature,
meta,
};

View File

@@ -1,14 +1,24 @@
import { SETTINGS_KEY } from '@/constants';
import { HTTP, ENV } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5';
import { getPolicyDescriptor } from '@/utils';
import $ from '@/core/app';
import headersResourceCache from '@/utils/headers-resource-cache';
export function getFlowField(headers) {
const subkey = Object.keys(headers).filter((k) =>
/SUBSCRIPTION-USERINFO/i.test(k),
)[0];
return headers[subkey];
const keys = Object.keys(headers);
let sub = '';
let webPage = '';
for (let k of keys) {
const lower = k.toLowerCase();
if (lower === 'subscription-userinfo') {
sub = headers[k];
} else if (lower === 'profile-web-page-url') {
webPage = headers[k];
}
}
return `${sub || ''}${webPage ? `;app_url=${webPage}` : ''}`;
}
export async function getFlowHeaders(
rawUrl,
@@ -41,30 +51,32 @@ export async function getFlowHeaders(
return;
}
const { isStash, isLoon, isShadowRocket, isQX } = ENV();
const cached = headersResourceCache.get(url);
const insecure = $arguments?.insecure
? $.env.isNode
? { strictSSL: false }
: { insecure: true }
: undefined;
const { defaultProxy, defaultFlowUserAgent, defaultTimeout } =
$.read(SETTINGS_KEY);
let proxy = customProxy || defaultProxy;
if ($.env.isNode) {
proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
}
const userAgent = ua || defaultFlowUserAgent || 'clash';
const requestTimeout = timeout || defaultTimeout || 8000;
const id = hex_md5(userAgent + url);
const cached = headersResourceCache.get(id);
let flowInfo;
if (!$arguments?.noCache && cached) {
// $.info(`使用缓存的流量信息: ${url}`);
$.info(`使用缓存的流量信息: ${url}, ${userAgent}`);
flowInfo = cached;
} else {
const { defaultProxy, defaultFlowUserAgent, defaultTimeout } =
$.read(SETTINGS_KEY);
let proxy = customProxy || defaultProxy;
if ($.env.isNode) {
proxy =
proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
}
const userAgent =
ua ||
defaultFlowUserAgent ||
'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)';
const requestTimeout = timeout || defaultTimeout;
const http = HTTP();
if (flowUrl) {
$.info(
`使用 GET 方法从响应体获取流量信息: ${flowUrl}, User-Agent: ${
userAgent || ''
}`,
}, Insecure: ${!!insecure}, Proxy: ${proxy}`,
);
const { body } = await http.get({
url: flowUrl,
@@ -72,6 +84,11 @@ export async function getFlowHeaders(
'User-Agent': userAgent,
},
timeout: requestTimeout,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
...(insecure ? insecure : {}),
});
flowInfo = body;
} else {
@@ -79,7 +96,7 @@ export async function getFlowHeaders(
$.info(
`使用 HEAD 方法从响应头获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}, Proxy: ${proxy}`,
}, Insecure: ${!!insecure}, Proxy: ${proxy}`,
);
const { headers } = await http.head({
url: url
@@ -103,20 +120,23 @@ export async function getFlowHeaders(
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
...(insecure ? insecure : {}),
});
flowInfo = getFlowField(headers);
} catch (e) {
$.error(
`使用 HEAD 方法从响应头获取流量信息失败: ${url}, User-Agent: ${
userAgent || ''
}, Proxy: ${proxy}: ${e.message ?? e}`,
}, Insecure: ${!!insecure}, Proxy: ${proxy}: ${
e.message ?? e
}`,
);
}
if (!flowInfo) {
$.info(
`使用 GET 方法获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}, Proxy: ${proxy}`,
}, Insecure: ${!!insecure}, Proxy: ${proxy}`,
);
const { headers } = await http.get({
url: url
@@ -140,12 +160,13 @@ export async function getFlowHeaders(
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
...(insecure ? insecure : {}),
});
flowInfo = getFlowField(headers);
}
}
if (flowInfo) {
headersResourceCache.set(url, flowInfo);
headersResourceCache.set(id, flowInfo);
}
}
@@ -176,8 +197,29 @@ export function parseFlowHeaders(flowHeaders) {
? Number(expireMatch[1] + expireMatch[2])
: undefined;
return { expires, total, usage: { upload, download } };
const remainingDaysMatch = flowHeaders.match(/reset_day=([0-9]+)/);
const remainingDays = remainingDaysMatch
? Number(remainingDaysMatch[1])
: undefined;
const appUrlMatch = flowHeaders.match(/app_url=(.*?)\s*?(;|$)/);
const appUrl = appUrlMatch ? decodeURIComponent(appUrlMatch[1]) : undefined;
const planNameMatch = flowHeaders.match(/plan_name=(.*?)\s*?(;|$)/);
const planName = planNameMatch
? decodeURIComponent(planNameMatch[1])
: undefined;
return {
expires,
total,
usage: { upload, download },
remainingDays,
appUrl,
planName,
};
}
export function flowTransfer(flow, unit = 'B') {
const unitList = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
let unitIndex = unitList.indexOf(unit);

View File

@@ -474,4 +474,7 @@ export class MMDB {
ipaso(ip) {
return this.asnReader?.asn(ip)?.autonomousSystemOrganization;
}
ipasn(ip) {
return this.asnReader?.asn(ip)?.autonomousSystemNumber;
}
}

View File

@@ -41,7 +41,7 @@ export default class Gist {
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
timeout,
timeout: timeout || 8000,
events: {
onResponse: (resp) => {
@@ -81,7 +81,7 @@ export default class Gist {
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
timeout,
timeout: timeout || 8000,
events: {
onResponse: (resp) => {

View File

@@ -2,18 +2,24 @@ export function getUserAgentFromHeaders(headers) {
const keys = Object.keys(headers);
let UA = '';
let ua = '';
let accept = '';
for (let k of keys) {
if (/USER-AGENT/i.test(k)) {
const lower = k.toLowerCase();
if (lower === 'user-agent') {
UA = headers[k];
ua = UA.toLowerCase();
break;
} else if (lower === 'accept') {
accept = headers[k];
}
}
return { UA, ua };
return { UA, ua, accept };
}
export function getPlatformFromUserAgent({ ua, UA }) {
export function getPlatformFromUserAgent({ ua, UA, accept }) {
if (UA.indexOf('Quantumult%20X') !== -1) {
return 'QX';
} else if (ua.indexOf('egern') !== -1) {
return 'Egern';
} else if (UA.indexOf('Surfboard') !== -1) {
return 'Surfboard';
} else if (UA.indexOf('Surge Mac') !== -1) {
@@ -39,11 +45,14 @@ export function getPlatformFromUserAgent({ ua, UA }) {
return 'V2Ray';
} else if (ua.indexOf('sing-box') !== -1) {
return 'sing-box';
} else {
} else if (accept.indexOf('application/json') === 0) {
return 'JSON';
} else {
return 'V2Ray';
}
}
export function getPlatformFromHeaders(headers) {
const { UA, ua } = getUserAgentFromHeaders(headers);
return getPlatformFromUserAgent({ ua, UA });
const { UA, ua, accept } = getUserAgentFromHeaders(headers);
return getPlatformFromUserAgent({ ua, UA, accept });
}

View File

@@ -9,6 +9,7 @@ export default function express({ substore: $, port, host }) {
'Access-Control-Allow-Methods': 'POST,GET,OPTIONS,PATCH,PUT,DELETE',
'Access-Control-Allow-Headers':
'Origin, X-Requested-With, Content-Type, Accept',
'X-Powered-By': 'Sub-Store',
};
// node support

View File

@@ -1,10 +1,10 @@
name: Sub-Store
description: "支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *"
description: '支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *'
compat_arguments:
ability: http-client-policy
cronexp: 55 23 * * *
sync: '"Sub-Store Sync"'
timeout: "120"
timeout: '120'
engine: auto
produce: '"# Sub-Store Produce"'
produce_cronexp: 50 */6 * * *
@@ -12,26 +12,27 @@ compat_arguments:
produce_col: '"col1,col2"'
compat_arguments_desc: '\n1⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 ''同步'' 或 ''同步配置''\n\n3⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4⃣ timeout\n\n脚本超时, 单位为秒\n\n5⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\n\n6⃣ produce\n\n自定义处理订阅的定时任务名\n一般用于定时处理耗时较长的订阅, 以更新缓存\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\n若设为 # 可取消此定时任务\n默认不开启\n\n7⃣ produce_cronexp\n\n配置处理订阅的定时任务\n\n默认为每 6 小时\n\n9⃣ produce_sub\n\n自定义需定时处理的单条订阅名\n多个用 , 连接\n\n🔟 produce_col\n\n自定义需定时处理的组合订阅名\n多个用 , 连接\n\n⚠ 注意: 是 名称(name) 不是 显示名称(displayName)\n如果名称需要编码, 请编码后再用 , 连接\n顺序: 并发执行单条订阅, 然后并发执行组合订阅'
scriptings:
- http_request:
name: Sub-Store Core
match: ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info)))
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js
body_required: true
- http_request:
name: Sub-Store Simple
match: ^https?:\/\/sub\.store
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js
body_required: true
- schedule:
name: "{{{sync}}}"
cron: "{{{cronexp}}}"
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
- schedule:
name: "{{{produce}}}"
cron: "{{{produce_cronexp}}}"
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
arguments:
_compat.$argument: '"sub={{{produce_sub}}}&col={{{produce_col}}}"'
- http_request:
name: Sub-Store Core
match: ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info)))
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js
body_required: true
- http_request:
name: Sub-Store Simple
match: ^https?:\/\/sub\.store
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js
body_required: true
- schedule:
name: '{{{sync}}}'
cron: '{{{cronexp}}}'
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
- schedule:
name: '{{{produce}}}'
cron: '{{{produce_cronexp}}}'
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
arguments:
_compat.$argument: '"sub={{{produce_sub}}}&col={{{produce_col}}}"'
mitm:
hostnames:
includes:
- sub.store

View File

@@ -67,6 +67,7 @@ function operator(proxies = [], targetPlatform, context) {
// getISO, // 获取 ISO 3166-1 alpha-2 代码
// Gist, // Gist 类
// download, // 内部的下载方法, 见 backend/src/utils/download.js
// MMDB, // Node.js 环境 可用于模拟 Surge/Loon 的 $utils.ipasn, $utils.ipaso, $utils.geoip. 具体见 https://t.me/zhetengsha/1269
// }
// 如果只是为了快速修改或者筛选 可以参考 脚本操作支持节点快捷脚本 https://t.me/zhetengsha/970 和 脚本筛选支持节点快捷脚本 https://t.me/zhetengsha/1009