@lancewu602/mihomo-dash 0.6.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/package.json +1 -1
- package/server/CHANGELOG.md +13 -0
- package/server/package.json +1 -1
- package/server/src/http/system-proxy.js +95 -6
- package/server/src/shared/bypass-list.js +86 -0
- package/server/src/shared/paths.js +3 -0
- package/server/src/shared/system-proxy.js +149 -12
- package/server/tests/system-proxy.test.js +235 -0
- package/web/dist/assets/{index-WmujD2EM.js → index-DX5KzSOH.js} +109 -109
- package/web/dist/favicon.svg +9 -4
- package/web/dist/index.html +3 -1
|
@@ -495,3 +495,238 @@ test('HTTP 端点:GET 读状态(含内核端口),PUT 开关,非法 bod
|
|
|
495
495
|
fs.rmSync(cfgDir, { recursive: true, force: true });
|
|
496
496
|
}
|
|
497
497
|
});
|
|
498
|
+
|
|
499
|
+
/** scutil 输出(协议开着 + 给定绕过列表),自定义项增删用例要知道写完之后的真实列表 */
|
|
500
|
+
const scutilOnWith = (bypass) => `<dictionary> {
|
|
501
|
+
ExceptionsList : <array> {
|
|
502
|
+
${bypass.map((b, i) => ` ${i} : ${b}`).join('\n')}
|
|
503
|
+
}
|
|
504
|
+
HTTPEnable : 1
|
|
505
|
+
HTTPPort : 7890
|
|
506
|
+
HTTPProxy : 127.0.0.1
|
|
507
|
+
HTTPSEnable : 1
|
|
508
|
+
HTTPSPort : 7890
|
|
509
|
+
HTTPSProxy : 127.0.0.1
|
|
510
|
+
ProxyAutoConfigEnable : 0
|
|
511
|
+
ProxyAutoDiscoveryEnable : 0
|
|
512
|
+
SOCKSEnable : 0
|
|
513
|
+
SOCKSPort : 0
|
|
514
|
+
}`;
|
|
515
|
+
|
|
516
|
+
/** 会记住 `-setproxybypassdomains` 的 macOS 桩:写完再读能读到新列表 */
|
|
517
|
+
function macOSBypassStub(initial) {
|
|
518
|
+
let bypass = [...initial];
|
|
519
|
+
const run = stubRun((cmd, args) => {
|
|
520
|
+
if (cmd === 'scutil') return { stdout: scutilOnWith(bypass) };
|
|
521
|
+
if (cmd === 'route') return { stdout: ROUTE_GET };
|
|
522
|
+
if (cmd === 'networksetup') {
|
|
523
|
+
if (args[0] === '-listnetworkserviceorder') return { stdout: SERVICE_ORDER };
|
|
524
|
+
if (args[0] === '-listallnetworkservices') return { stdout: ALL_SERVICES };
|
|
525
|
+
if (args[0] === '-setproxybypassdomains') bypass = args.slice(2);
|
|
526
|
+
return { stdout: '' };
|
|
527
|
+
}
|
|
528
|
+
return { ok: false, stderr: `unexpected: ${cmd}` };
|
|
529
|
+
});
|
|
530
|
+
return { run, bypass: () => bypass };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
test('normalizeBypassEntry:域名 / 通配 / IP / 网段都收,端口与乱写挡在写系统设置之前', () => {
|
|
534
|
+
const { normalizeBypassEntry } = mods.shared;
|
|
535
|
+
// 收:单标签主机名、带子域、通配三种写法(*. / +. / .)、FQDN 尾点、IPv4/IPv6、两种网段
|
|
536
|
+
for (const [raw, want] of [
|
|
537
|
+
['example.com', 'example.com'],
|
|
538
|
+
[' Sub.Corp.Example. ', 'sub.corp.example'],
|
|
539
|
+
['*.corp.example', '*.corp.example'],
|
|
540
|
+
['+.corp.example', '+.corp.example'],
|
|
541
|
+
['.corp.example', '.corp.example'],
|
|
542
|
+
['myhost', 'myhost'],
|
|
543
|
+
['192.168.1.5', '192.168.1.5'],
|
|
544
|
+
['10.0.0.0/8', '10.0.0.0/8'],
|
|
545
|
+
['192.168.1.5/32', '192.168.1.5/32'],
|
|
546
|
+
['::1', '::1'],
|
|
547
|
+
['fe80::/10', 'fe80::/10'],
|
|
548
|
+
['FD00::1/8', 'fd00::1/8'],
|
|
549
|
+
]) {
|
|
550
|
+
assert.equal(normalizeBypassEntry(raw), want, `${raw} 应该被接受`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// 拒:空、协议 / 路径、端口、前导 -、空格、非法 IP、越界前缀、空标签、乱放通配、非 ASCII
|
|
554
|
+
for (const raw of [
|
|
555
|
+
'',
|
|
556
|
+
' ',
|
|
557
|
+
'.',
|
|
558
|
+
'http://example.com',
|
|
559
|
+
'example.com/path',
|
|
560
|
+
'example.com:8080',
|
|
561
|
+
'-example.com',
|
|
562
|
+
'--setwebproxystate',
|
|
563
|
+
'a b.com',
|
|
564
|
+
'256.1.1.1',
|
|
565
|
+
'1.2.3.4.5.6.7.8',
|
|
566
|
+
'10.0.0.0/33',
|
|
567
|
+
'10.0.0.0/',
|
|
568
|
+
'/8',
|
|
569
|
+
'fe80::/129',
|
|
570
|
+
'fe80::1/8/8',
|
|
571
|
+
'a..b',
|
|
572
|
+
'*',
|
|
573
|
+
'a*b.com',
|
|
574
|
+
'foo.*.com',
|
|
575
|
+
'例子.com',
|
|
576
|
+
'example.com#x',
|
|
577
|
+
'example.com?x',
|
|
578
|
+
'[::1]:8080',
|
|
579
|
+
'-',
|
|
580
|
+
'*' + 'a'.repeat(300),
|
|
581
|
+
]) {
|
|
582
|
+
assert.throws(
|
|
583
|
+
() => normalizeBypassEntry(raw),
|
|
584
|
+
/不合法|不能|只能|过长|网段|通配|要有|只有一个点/,
|
|
585
|
+
`${JSON.stringify(raw)} 应该被拒`,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test('mergeBypass:自定义项排在固定项前,与系统既有项大小写不敏感去重', () => {
|
|
591
|
+
const { mergeBypass, BYPASS_DEFAULTS } = mods.shared;
|
|
592
|
+
const { list, added } = mergeBypass(
|
|
593
|
+
['LOCALHOST', 'old.example'],
|
|
594
|
+
['*.Corp.Example', 'old.example', '*.local'],
|
|
595
|
+
);
|
|
596
|
+
assert.deepEqual(list.slice(0, 2), ['LOCALHOST', 'old.example'], '系统里已有的原样保留');
|
|
597
|
+
assert.deepEqual(added.slice(0, 1), ['*.Corp.Example'], '自定义项排在固定项前面');
|
|
598
|
+
assert.equal(
|
|
599
|
+
list.filter((v) => v.toLowerCase() === '*.local').length,
|
|
600
|
+
1,
|
|
601
|
+
'自定义项与固定项同名时只留一份',
|
|
602
|
+
);
|
|
603
|
+
assert.equal(
|
|
604
|
+
added.length,
|
|
605
|
+
1 + 1 + BYPASS_DEFAULTS.filter((d) => !['localhost', '*.local'].includes(d.value)).length,
|
|
606
|
+
'自定义项各一条 + 缺的固定项',
|
|
607
|
+
);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test('setBypass:把调用方算好的整套列表写进系统(macOS argv / GNOME ignore-hosts)', async () => {
|
|
611
|
+
const stub = macOSBypassStub(['localhost', '127.0.0.0/8']);
|
|
612
|
+
const mac = mods.shared.createSystemProxy({ run: stub.run, platform: 'darwin' });
|
|
613
|
+
const wrote = await mac.setBypass(['localhost', '127.0.0.0/8', '*.corp.example', '*.local']);
|
|
614
|
+
assert.equal(wrote.service, 'USB 10/100 LAN');
|
|
615
|
+
assert.deepEqual(stub.bypass(), ['localhost', '127.0.0.0/8', '*.corp.example', '*.local']);
|
|
616
|
+
await assert.rejects(() => mac.setBypass([]), /不能是空的/);
|
|
617
|
+
|
|
618
|
+
const linuxRun = stubRun((cmd, args) => {
|
|
619
|
+
assert.equal(cmd, 'gsettings');
|
|
620
|
+
if (args[0] === 'get') return { stdout: "'manual'" };
|
|
621
|
+
return { stdout: '' };
|
|
622
|
+
});
|
|
623
|
+
const linux = mods.shared.createSystemProxy({ run: linuxRun, platform: 'linux' });
|
|
624
|
+
const gnome = await linux.setBypass(['127.0.0.1', '*.corp.example']);
|
|
625
|
+
assert.equal(gnome.service, 'GNOME');
|
|
626
|
+
assert.ok(
|
|
627
|
+
linuxRun
|
|
628
|
+
.commands()
|
|
629
|
+
.includes(
|
|
630
|
+
`gsettings set org.gnome.system.proxy ignore-hosts ['127.0.0.1', '*.corp.example']`,
|
|
631
|
+
),
|
|
632
|
+
`GVariant 写法不对:${linuxRun.commands().join(' | ')}`,
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
const win = mods.shared.createSystemProxy({ run: stubRun(() => ({})), platform: 'win32' });
|
|
636
|
+
await assert.rejects(() => win.setBypass(['a.example']), /Internet 选项/);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test('HTTP 端点:自定义绕过项能加能删,只动自己那一条,非法值 400', async () => {
|
|
640
|
+
const kernel = http.createServer((_req, res) => {
|
|
641
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
642
|
+
res.end(JSON.stringify({ 'mixed-port': 7890, mode: 'direct' }));
|
|
643
|
+
});
|
|
644
|
+
await new Promise((resolve) => kernel.listen(0, '127.0.0.1', resolve));
|
|
645
|
+
const cfgDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mihomo-dash-sysproxy-cfg-'));
|
|
646
|
+
fs.writeFileSync(
|
|
647
|
+
path.join(cfgDir, 'config.yaml'),
|
|
648
|
+
`mixed-port: 7890\nexternal-controller: 127.0.0.1:${kernel.address().port}\n`,
|
|
649
|
+
);
|
|
650
|
+
process.env.MIHOMO_DIR = cfgDir;
|
|
651
|
+
mods.paths.resetMihomoDirCache();
|
|
652
|
+
mods.kernel.resetControllerCache();
|
|
653
|
+
|
|
654
|
+
const stub = macOSBypassStub(['localhost', '127.0.0.0/8', '*.local']);
|
|
655
|
+
const app = express();
|
|
656
|
+
app.use(express.json());
|
|
657
|
+
mods.routes.registerSystemProxyRoutes(app, { run: stub.run, platform: 'darwin' });
|
|
658
|
+
const server = app.listen(0, '127.0.0.1');
|
|
659
|
+
await new Promise((resolve) => server.once('listening', resolve));
|
|
660
|
+
const base = `http://127.0.0.1:${server.address().port}`;
|
|
661
|
+
const storeFile = path.join(mods.paths.CLI_DIR, 'system-proxy.json');
|
|
662
|
+
const post = (value) =>
|
|
663
|
+
fetch(`${base}/api/system-proxy/bypass`, {
|
|
664
|
+
method: 'POST',
|
|
665
|
+
headers: { 'content-type': 'application/json' },
|
|
666
|
+
body: JSON.stringify({ value }),
|
|
667
|
+
});
|
|
668
|
+
const del = (value) =>
|
|
669
|
+
fetch(`${base}/api/system-proxy/bypass?value=${encodeURIComponent(value)}`, {
|
|
670
|
+
method: 'DELETE',
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
try {
|
|
674
|
+
// 加一条:规范化后落盘,并立刻写进系统绕过列表(现有 + 自定义 + 缺的固定项)
|
|
675
|
+
const added = await post('Corp.Example.');
|
|
676
|
+
assert.equal(added.status, 200);
|
|
677
|
+
const addedBody = await added.json();
|
|
678
|
+
assert.equal(addedBody.added, 'corp.example');
|
|
679
|
+
assert.deepEqual(addedBody.bypassCustom, ['corp.example']);
|
|
680
|
+
assert.ok(addedBody.applied.includes('corp.example'));
|
|
681
|
+
assert.deepEqual(
|
|
682
|
+
JSON.parse(fs.readFileSync(storeFile, 'utf8')),
|
|
683
|
+
{ custom: ['corp.example'] },
|
|
684
|
+
'自定义项要落进状态目录',
|
|
685
|
+
);
|
|
686
|
+
assert.deepEqual(stub.bypass().slice(0, 4), [
|
|
687
|
+
'localhost',
|
|
688
|
+
'127.0.0.0/8',
|
|
689
|
+
'*.local',
|
|
690
|
+
'corp.example',
|
|
691
|
+
]);
|
|
692
|
+
assert.ok(addedBody.bypass.includes('corp.example'), '写完之后 GET 状态里能看到它');
|
|
693
|
+
|
|
694
|
+
// 重复 / 非法:都是 400,且不改系统设置、不落盘
|
|
695
|
+
assert.equal((await post('corp.example')).status, 400);
|
|
696
|
+
const badBody = await (await post('example.com:8080')).json();
|
|
697
|
+
assert.match(badBody.error, /不合法|只能|不支持/);
|
|
698
|
+
assert.deepEqual(stub.bypass().slice(0, 4), [
|
|
699
|
+
'localhost',
|
|
700
|
+
'127.0.0.0/8',
|
|
701
|
+
'*.local',
|
|
702
|
+
'corp.example',
|
|
703
|
+
]);
|
|
704
|
+
|
|
705
|
+
// Python 式大礼包也进不去(不会变成系统命令的参数)
|
|
706
|
+
assert.equal((await post('-setwebproxystate')).status, 400);
|
|
707
|
+
|
|
708
|
+
// 删掉:从列表与系统里都移除,其它条目一条不动
|
|
709
|
+
const removed = await del('CORP.example');
|
|
710
|
+
assert.equal(removed.status, 200);
|
|
711
|
+
const removedBody = await removed.json();
|
|
712
|
+
assert.equal(removedBody.removed, 'corp.example');
|
|
713
|
+
assert.deepEqual(removedBody.bypassCustom, []);
|
|
714
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(storeFile, 'utf8')), { custom: [] });
|
|
715
|
+
assert.deepEqual(stub.bypass().slice(0, 3), ['localhost', '127.0.0.0/8', '*.local']);
|
|
716
|
+
assert.ok(!stub.bypass().includes('corp.example'), '系统列表里也要没了');
|
|
717
|
+
|
|
718
|
+
// 删不存在的:400(页面据此提示刷新)
|
|
719
|
+
const missing = await del('corp.example');
|
|
720
|
+
assert.equal(missing.status, 400);
|
|
721
|
+
assert.match((await missing.json()).error, /没有这一条/);
|
|
722
|
+
|
|
723
|
+
// 空 value:400
|
|
724
|
+
assert.equal((await del(' ')).status, 400);
|
|
725
|
+
} finally {
|
|
726
|
+
server.close();
|
|
727
|
+
kernel.close();
|
|
728
|
+
delete process.env.MIHOMO_DIR;
|
|
729
|
+
fs.rmSync(cfgDir, { recursive: true, force: true });
|
|
730
|
+
fs.rmSync(storeFile, { force: true });
|
|
731
|
+
}
|
|
732
|
+
});
|