Skip to content

Commit 946cdaf

Browse files
committed
feat: mihomo VLESS XHTTP 下行 阻断 reality-opts 继承,补充 Shadowrocket XHTTP 过滤与提示
1 parent 89767ce commit 946cdaf

5 files changed

Lines changed: 242 additions & 4 deletions

File tree

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sub-store",
3-
"version": "2.22.7",
3+
"version": "2.22.8",
44
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
55
"main": "src/main.js",
66
"scripts": {

backend/src/core/proxy-utils/producers/clashmeta.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
produceProxyListOutput,
55
supportsShadowsocksV2rayPluginMode,
66
} from '@/core/proxy-utils/producers/utils';
7+
import $ from '@/core/app';
78

89
const ipVersions = {
910
dual: 'dual',
@@ -19,9 +20,7 @@ export default function ClashMeta_Producer() {
1920
const list = proxies
2021
.filter((proxy) => {
2122
if (opts['include-unsupported-proxy']) return true;
22-
if (
23-
!supportsShadowsocksV2rayPluginMode(proxy, ['websocket'])
24-
) {
23+
if (!supportsShadowsocksV2rayPluginMode(proxy, ['websocket'])) {
2524
return false;
2625
} else if (proxy.type === 'snell' && proxy.version >= 4) {
2726
return false;
@@ -182,6 +181,24 @@ export default function ClashMeta_Producer() {
182181
// 且不能启用 Reality;这条限制同时作用于根 xhttp
183182
// 配置,以及继承根配置默认值后的嵌套
184183
// `download-settings`。
184+
185+
// 1. mihomo 似乎不支持上行/下行有一个为 tls 一个不为 tls. 但是这跟转换无关了
186+
// 2. 下行有 tls 且上行有 reality-opts 且下行无 reality-opts →
187+
// 补 reality-opts: { public-key: '' },阻断 reality 继承
188+
if (
189+
proxy.network === 'xhttp' &&
190+
proxy['xhttp-opts']?.['download-settings']
191+
) {
192+
const ds = proxy['xhttp-opts']['download-settings'];
193+
if (
194+
proxy.tls &&
195+
ds.tls &&
196+
proxy['reality-opts'] &&
197+
!ds['reality-opts']
198+
) {
199+
ds['reality-opts'] = { 'public-key': '' };
200+
}
201+
}
185202
} else if (proxy.type === 'ss') {
186203
if (
187204
isPresent(proxy, 'shadow-tls-password') &&

backend/src/core/proxy-utils/producers/shadowrocket.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ export default function Shadowrocket_Producer() {
5151
) {
5252
return false;
5353
} else if (['xhttp'].includes(proxy.network)) {
54+
$.info(
55+
`Shadowrocket 不支持从 mihomo 格式读取 XHTTP, 请使用 V2Ray 格式输出`,
56+
);
5457
return false;
5558
}
5659
return true;

backend/src/test/proxy-producers/structured.spec.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1694,4 +1694,154 @@ describe('Proxy structured producers', function () {
16941694
name: 'JSON HTTP',
16951695
});
16961696
});
1697+
1698+
it('blocks Mihomo xhttp reality-opts inheritance when upload has reality-opts but download does not', function () {
1699+
const proxy = {
1700+
type: 'vless',
1701+
name: 'XHTTP Reality No Download Reality',
1702+
server: 'vless-xhttp.example.com',
1703+
port: 443,
1704+
uuid: UUID,
1705+
tls: true,
1706+
sni: 'sni.example.com',
1707+
network: 'xhttp',
1708+
'reality-opts': {
1709+
'public-key': 'upload-pubkey',
1710+
'short-id': 'ab',
1711+
},
1712+
'xhttp-opts': {
1713+
path: '/xhttp',
1714+
mode: 'stream-up',
1715+
'download-settings': {
1716+
server: 'download.example.com',
1717+
port: 8443,
1718+
tls: true,
1719+
path: '/download',
1720+
},
1721+
},
1722+
};
1723+
1724+
const internal = produceInternal('Mihomo', proxy);
1725+
const external = loadProducedYaml('Mihomo', proxy);
1726+
1727+
expect(internal).to.have.length(1);
1728+
expectSubset(internal[0], {
1729+
'xhttp-opts': {
1730+
'download-settings': {
1731+
'reality-opts': { 'public-key': '' },
1732+
},
1733+
},
1734+
});
1735+
expectSubset(external.proxies[0], {
1736+
'xhttp-opts': {
1737+
'download-settings': {
1738+
'reality-opts': { 'public-key': '' },
1739+
},
1740+
},
1741+
});
1742+
});
1743+
1744+
it('does not inject reality-opts blocker when download-settings already has reality-opts', function () {
1745+
const proxy = {
1746+
type: 'vless',
1747+
name: 'XHTTP Reality Both',
1748+
server: 'vless-xhttp.example.com',
1749+
port: 443,
1750+
uuid: UUID,
1751+
tls: true,
1752+
sni: 'sni.example.com',
1753+
network: 'xhttp',
1754+
'reality-opts': {
1755+
'public-key': 'upload-pubkey',
1756+
'short-id': 'ab',
1757+
},
1758+
'xhttp-opts': {
1759+
path: '/xhttp',
1760+
mode: 'stream-up',
1761+
'download-settings': {
1762+
server: 'download.example.com',
1763+
port: 8443,
1764+
tls: true,
1765+
path: '/download',
1766+
'reality-opts': {
1767+
'public-key': 'download-pubkey',
1768+
'short-id': 'cd',
1769+
},
1770+
},
1771+
},
1772+
};
1773+
1774+
const internal = produceInternal('Mihomo', proxy);
1775+
1776+
expect(internal).to.have.length(1);
1777+
expectSubset(internal[0], {
1778+
'xhttp-opts': {
1779+
'download-settings': {
1780+
'reality-opts': {
1781+
'public-key': 'download-pubkey',
1782+
'short-id': 'cd',
1783+
},
1784+
},
1785+
},
1786+
});
1787+
});
1788+
1789+
it('does not inject reality-opts blocker when upload has no reality-opts', function () {
1790+
const proxy = {
1791+
type: 'vless',
1792+
name: 'XHTTP No Reality',
1793+
server: 'vless-xhttp.example.com',
1794+
port: 443,
1795+
uuid: UUID,
1796+
tls: true,
1797+
sni: 'sni.example.com',
1798+
network: 'xhttp',
1799+
'xhttp-opts': {
1800+
path: '/xhttp',
1801+
mode: 'stream-up',
1802+
'download-settings': {
1803+
server: 'download.example.com',
1804+
port: 8443,
1805+
tls: true,
1806+
path: '/download',
1807+
},
1808+
},
1809+
};
1810+
1811+
const internal = produceInternal('Mihomo', proxy);
1812+
1813+
expect(internal).to.have.length(1);
1814+
const ds = internal[0]['xhttp-opts']['download-settings'];
1815+
expect(ds).to.not.have.property('reality-opts');
1816+
});
1817+
1818+
it('filters xhttp proxies from Shadowrocket by default', function () {
1819+
const proxies = [
1820+
{
1821+
type: 'vless',
1822+
name: 'VLESS XHTTP',
1823+
server: 'vless.example.com',
1824+
port: 443,
1825+
uuid: UUID,
1826+
tls: true,
1827+
network: 'xhttp',
1828+
'xhttp-opts': { path: '/xhttp' },
1829+
},
1830+
{
1831+
type: 'vless',
1832+
name: 'VLESS WS',
1833+
server: 'vless.example.com',
1834+
port: 443,
1835+
uuid: UUID,
1836+
tls: true,
1837+
network: 'ws',
1838+
'ws-opts': { path: '/ws' },
1839+
},
1840+
];
1841+
1842+
const internal = produceInternal('Shadowrocket', proxies);
1843+
1844+
expect(internal.map((p) => p.name)).to.deep.equal(['VLESS WS']);
1845+
});
1846+
16971847
});

backend/src/test/restful/logs.spec.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,4 +429,72 @@ describe('logs routes', function () {
429429
expect(JSON.parse(state[LOGS_KEY])).to.deep.equal([]);
430430
expect(clearLogEntries).to.be.a('function');
431431
});
432+
433+
it('rejects negative logsMaxCount in settings PATCH', async function () {
434+
const app = createRouteApp();
435+
registerSettingsRoutes(app);
436+
const patchHandler = app.handlers.get('PATCH /api/settings');
437+
438+
const res = createResponse('/api/settings');
439+
await patchHandler({ body: { logsMaxCount: -1 } }, res);
440+
441+
expect(res.body.status).to.equal('success');
442+
expect(state[SETTINGS_KEY]).to.not.have.property('logsMaxCount');
443+
});
444+
445+
it('rejects NaN logsMaxCount in settings PATCH', async function () {
446+
const app = createRouteApp();
447+
registerSettingsRoutes(app);
448+
const patchHandler = app.handlers.get('PATCH /api/settings');
449+
450+
const res = createResponse('/api/settings');
451+
await patchHandler({ body: { logsMaxCount: 'abc' } }, res);
452+
453+
expect(res.body.status).to.equal('success');
454+
expect(state[SETTINGS_KEY]).to.not.have.property('logsMaxCount');
455+
});
456+
457+
it('rejects null logsMaxCount in settings PATCH', async function () {
458+
const app = createRouteApp();
459+
registerSettingsRoutes(app);
460+
const patchHandler = app.handlers.get('PATCH /api/settings');
461+
462+
const res = createResponse('/api/settings');
463+
await patchHandler({ body: { logsMaxCount: null } }, res);
464+
465+
expect(res.body.status).to.equal('success');
466+
expect(state[SETTINGS_KEY]).to.not.have.property('logsMaxCount');
467+
});
468+
469+
it('accepts valid positive integer logsMaxCount in settings PATCH', async function () {
470+
const app = createRouteApp();
471+
registerSettingsRoutes(app);
472+
const patchHandler = app.handlers.get('PATCH /api/settings');
473+
474+
const res = createResponse('/api/settings');
475+
await patchHandler({ body: { logsMaxCount: 300 } }, res);
476+
477+
expect(res.body.status).to.equal('success');
478+
expect(state[SETTINGS_KEY]).to.deep.include({ logsMaxCount: 300 });
479+
});
480+
481+
it('clears log settings cache after settings PATCH so new logsMaxCount takes effect immediately', async function () {
482+
const app = createRouteApp();
483+
registerSettingsRoutes(app);
484+
const patchHandler = app.handlers.get('PATCH /api/settings');
485+
486+
state[SETTINGS_KEY] = { logsMaxCount: 500 };
487+
appendLogEntry($, 'info', ['before change']);
488+
appendLogEntry($, 'info', ['before change 2']);
489+
appendLogEntry($, 'info', ['before change 3']);
490+
491+
await patchHandler({ body: { logsMaxCount: 1 } }, createResponse('/api/settings'));
492+
493+
appendLogEntry($, 'info', ['after change']);
494+
495+
const storedLogs = JSON.parse(state[LOGS_KEY]);
496+
expect(storedLogs).to.have.length(1);
497+
expect(storedLogs[0].message).to.equal('[unknown] INFO: after change');
498+
});
499+
432500
});

0 commit comments

Comments
 (0)