Compare commits

..

12 Commits

Author SHA1 Message Date
xream
95b7557635 feat: Loon 正式支持 Shadowsocks 2022
Some checks failed
build / build (push) Has been cancelled
2024-12-31 23:24:35 +08:00
xream
14ca62db4a doc: demo.js
Some checks are pending
build / build (push) Waiting to run
2024-12-31 15:28:32 +08:00
xream
a2a754adb7 feat: sing-box 支持使用 _network 字段来设置 network 2024-12-31 15:27:14 +08:00
xream
6b23f82953 fix: 修复代理 App 版中路由 target 参数为空的情况
Some checks are pending
build / build (push) Waiting to run
2024-12-30 21:36:45 +08:00
xream
e071a7f253 feat: 组合订阅和文件的导出导入 2024-12-30 21:21:18 +08:00
xream
b9bba895e1 feat: 支持订阅级别的 noCache 2024-12-29 23:37:07 +08:00
xream
8090d678ee feat: 分享支持多一级路由指定输出目标 2024-12-29 22:08:24 +08:00
xream
ff4be7ac38 feat: 订阅支持开关 passThroughUA 透传请求的 User-Agent 2024-12-29 21:33:15 +08:00
xream
7e2109dc68 feat: 支持订阅参数 url 同时支持单条本地节点内容, 支持多一级路由指定输出目标 2024-12-29 21:03:52 +08:00
xream
278beae99a feat: 支持 Egern 前置代理 prev_hop 和 Hysteria2 端口跳跃 2024-12-29 20:05:55 +08:00
xream
3aedd5943d feat: sing-box includeUnsupportedProxy 开启支持 Hysteria2 端口跳跃 2024-12-29 16:07:33 +08:00
xream
222551eb20 feat: Egern 增加默认 sni 2024-12-28 21:00:40 +08:00
9 changed files with 123 additions and 23 deletions

View File

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

View File

@@ -72,6 +72,15 @@ export default function Egern_Producer() {
return true;
})
.map((proxy) => {
if (proxy.tls && !proxy.sni) {
proxy.sni = proxy.server;
}
const prev_hop =
proxy.prev_hop ||
proxy['underlying-proxy'] ||
proxy['dialer-proxy'] ||
proxy.detour;
if (proxy.type === 'http') {
proxy = {
type: 'http',
@@ -130,6 +139,8 @@ export default function Egern_Producer() {
next_hop: proxy.next_hop,
sni: proxy.sni,
skip_tls_verify: proxy['skip-cert-verify'],
port_hopping: proxy.ports,
port_hopping_interval: proxy['hop-interval'],
};
if (proxy['obfs-password'] && proxy.obfs == 'salamander') {
proxy.obfs = 'salamander';
@@ -284,6 +295,7 @@ export default function Egern_Producer() {
[proxy.type]: {
...proxy,
type: undefined,
prev_hop,
},
};
});

View File

@@ -32,7 +32,7 @@ export default function Loon_Producer() {
return { produce };
}
function shadowsocks(proxy, includeUnsupportedProxy) {
function shadowsocks(proxy) {
const result = new Result(proxy);
if (
![
@@ -56,9 +56,8 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
'aes-256-gcm',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
...(includeUnsupportedProxy
? ['2022-blake3-aes-128-gcm', '2022-blake3-aes-256-gcm']
: []),
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
].includes(proxy.cipher)
) {
throw new Error(`cipher ${proxy.cipher} is not supported`);

View File

@@ -5,6 +5,10 @@ import { isIPv4, isIPv6 } from '@/utils';
const detourParser = (proxy, parsedProxy) => {
parsedProxy.detour = proxy['dialer-proxy'] || proxy.detour;
};
const networkParser = (proxy, parsedProxy) => {
if (['tcp', 'udp'].includes(proxy._network))
parsedProxy.network = proxy._network;
};
const tfoParser = (proxy, parsedProxy) => {
parsedProxy.tcp_fast_open = false;
if (proxy.tfo) parsedProxy.tcp_fast_open = true;
@@ -305,6 +309,7 @@ const socks5Parser = (proxy = {}) => {
if (proxy.uot) parsedProxy.udp_over_tcp = true;
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
return parsedProxy;
@@ -356,6 +361,7 @@ const ssParser = (proxy = {}) => {
if (proxy.uot) parsedProxy.udp_over_tcp = true;
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
@@ -470,7 +476,7 @@ const vmessParser = (proxy = {}) => {
if (proxy.network === 'h2') h2Parser(proxy, parsedProxy);
if (proxy.network === 'http') h1Parser(proxy, parsedProxy);
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
@@ -493,7 +499,7 @@ const vlessParser = (proxy = {}) => {
if (proxy.flow === 'xtls-rprx-vision') parsedProxy.flow = proxy.flow;
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
@@ -514,7 +520,7 @@ const trojanParser = (proxy = {}) => {
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
@@ -562,13 +568,14 @@ const hysteriaParser = (proxy = {}) => {
parsedProxy.disable_mtu_discovery = true;
}
}
networkParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const hysteria2Parser = (proxy = {}) => {
const hysteria2Parser = (proxy = {}, includeUnsupportedProxy) => {
const parsedProxy = {
tag: proxy.name,
type: 'hysteria2',
@@ -580,12 +587,23 @@ const hysteria2Parser = (proxy = {}) => {
};
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (includeUnsupportedProxy) {
if (proxy['hop-interval'])
parsedProxy.hop_interval = /^\d+$/.test(proxy['hop-interval'])
? `${proxy['hop-interval']}s`
: proxy['hop-interval'];
if (proxy['ports'])
parsedProxy.server_ports = proxy['ports']
.split(/\s*,\s*/)
.map((p) => p.replace(/\s*-\s*/g, ':'));
}
if (proxy.up) parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
if (proxy.down) parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
if (proxy.obfs === 'salamander') parsedProxy.obfs.type = 'salamander';
if (proxy['obfs-password'])
parsedProxy.obfs.password = proxy['obfs-password'];
if (!parsedProxy.obfs.type) delete parsedProxy.obfs;
networkParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
@@ -616,6 +634,7 @@ const tuic5Parser = (proxy = {}) => {
if (proxy['udp-over-stream']) parsedProxy.udp_over_stream = true;
if (proxy['heartbeat-interval'])
parsedProxy.heartbeat = `${proxy['heartbeat-interval']}ms`;
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
@@ -673,6 +692,7 @@ const wireguardParser = (proxy = {}) => {
parsedProxy.peers.push(peer);
}
}
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
@@ -790,7 +810,12 @@ export default function singbox_Producer() {
list.push(hysteriaParser(proxy));
break;
case 'hysteria2':
list.push(hysteria2Parser(proxy));
list.push(
hysteria2Parser(
proxy,
opts['include-unsupported-proxy'],
),
);
break;
case 'tuic':
if (!proxy.token || proxy.token.length === 0) {

View File

@@ -50,11 +50,21 @@ function createCollection(req, res) {
function getCollection(req, res) {
let { name } = req.params;
let { raw } = req.query;
name = decodeURIComponent(name);
const allCols = $.read(COLLECTIONS_KEY);
const collection = findByName(allCols, name);
if (collection) {
success(res, collection);
if (raw) {
res.set('content-type', 'application/json')
.set(
'content-disposition',
`attachment; filename="${encodeURIComponent(name)}.json"`,
)
.send(JSON.stringify(collection));
} else {
success(res, collection);
}
} else {
failed(
res,

View File

@@ -13,11 +13,44 @@ import { getISO } from '@/utils/geo';
import env from '@/utils/env';
export default function register($app) {
$app.get('/share/col/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadCollection(req, res);
});
$app.get('/share/col/:name', downloadCollection);
$app.get('/share/sub/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadSubscription(req, res);
});
$app.get('/share/sub/:name', downloadSubscription);
$app.get('/download/collection/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadCollection(req, res);
});
$app.get('/download/collection/:name', downloadCollection);
$app.get('/download/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadSubscription(req, res);
});
$app.get('/download/:name', downloadSubscription);
$app.get(
'/download/collection/:name/api/v1/server/details',
async (req, res) => {
@@ -59,11 +92,9 @@ async function downloadSubscription(req, res) {
const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
$.info(
`正在下载订阅:${name}\n请求 User-Agent: ${
req.headers['user-agent'] || req.headers['User-Agent']
}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
`正在下载订阅:${name}\n请求 User-Agent: ${reqUA}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
);
let {
url,
@@ -98,6 +129,14 @@ async function downloadSubscription(req, res) {
if (url) {
url = decodeURIComponent(url);
$.info(`指定远程订阅 URL: ${url}`);
if (!/^https?:\/\//.test(url)) {
content = url;
$.info(`URL 不是链接,视为本地订阅`);
}
}
if (content) {
content = decodeURIComponent(content);
$.info(`指定本地订阅: ${content}`);
}
if (proxy) {
proxy = decodeURIComponent(proxy);
@@ -107,10 +146,7 @@ async function downloadSubscription(req, res) {
ua = decodeURIComponent(ua);
$.info(`指定远程订阅 User-Agent: ${ua}`);
}
if (content) {
content = decodeURIComponent(content);
$.info(`指定本地订阅: ${content}`);
}
if (mergeSources) {
mergeSources = decodeURIComponent(mergeSources);
$.info(`指定合并来源: ${mergeSources}`);
@@ -140,6 +176,13 @@ async function downloadSubscription(req, res) {
const sub = findByName(allSubs, name);
if (sub) {
try {
const passThroughUA = sub.passThroughUA;
if (passThroughUA) {
$.info(
`订阅开启了透传 User-Agent, 使用请求的 User-Agent: ${reqUA}`,
);
ua = reqUA;
}
let output = await produceArtifact({
type: 'subscription',
name,

View File

@@ -197,11 +197,21 @@ async function getFile(req, res) {
}
function getWholeFile(req, res) {
let { name } = req.params;
let { raw } = req.query;
name = decodeURIComponent(name);
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
if (file) {
success(res, file);
if (raw) {
res.set('content-type', 'application/json')
.set(
'content-disposition',
`attachment; filename="${encodeURIComponent(name)}.json"`,
)
.send(JSON.stringify(file));
} else {
success(res, file);
}
} else {
failed(
res,

View File

@@ -73,7 +73,7 @@ async function produceArtifact({
proxy || sub.proxy,
undefined,
awaitCustomCache,
noCache,
noCache || sub.noCache,
true,
);
} catch (err) {
@@ -122,7 +122,7 @@ async function produceArtifact({
proxy || sub.proxy,
undefined,
awaitCustomCache,
noCache,
noCache || sub.noCache,
true,
);
} catch (err) {
@@ -244,7 +244,7 @@ async function produceArtifact({
collection.proxy,
undefined,
undefined,
noCache,
noCache || sub.noCache,
true,
);
} catch (err) {

View File

@@ -21,6 +21,7 @@ function operator(proxies = [], targetPlatform, context) {
// 13. `test-url` 为测延迟链接, `test-timeout` 为测延迟超时
// 14. `ports` 为端口跳跃, `hop-interval` 变换端口号的时间间隔
// 15. `ip-version` 设置节点使用 IP 版本可选dualipv4ipv6ipv4-preferipv6-prefer. 会进行内部转换, 若无法匹配则使用原始值
// 16. `sing-box` 支持使用 `_network` 来设置 `network`, 例如 `tcp`, `udp`
// require 为 Node.js 的 require, 在 Node.js 运行环境下 可以用来引入模块