一套跑在 Debian 13 (trixie) 上的软路由管理系统,提供 43 个功能页面的 Web 管理面板:WAN 接入、PPPoE 多拨、DHCP/DNS、IPv6、防火墙、QoS 限速、 访问控制、DDNS、文件共享、Docker 管理等。 核心设计原则:不重新实现任何协议 PPPoE 交给 ppp,DHCP 交给 dnsmasq,IPv6 RA 交给 radvd,NAT 交给 nftables。 Web 面板只做三件事:渲染原生配置文件 → 语法预检 → 原子写入并 reload。 面板删掉后,系统仍是一台配置完好的 Debian 路由器。 安全默认值 · 安装后不接管网络:不改默认路由、不开 IP 转发、不启动 DHCP 服务 · 所有改网络的动作都需要用户在面板上亲手点「应用」 · 避开 5900 端口,不触碰 VNC / X 会话 · 配置写入前先做语法预检,失败则回滚 架构 · backend/ Python 3 标准库,零第三方依赖(9 个模块) · web/ 原生 JS/CSS,无构建步骤 · 常驻 helper 通过 Unix socket 提供 75 个动作,socket 不可用时回退 subprocess · 鉴权走 X-Token 请求头 打包 · packaging/build-deb.sh → drouter_1.0.0_all.deb(含 6 个 systemd 单元) · scripts/make-offline-deps.sh → 236 个依赖 .deb + 本地仓库索引 · packaging/build-offline-bundle.sh → 自包含离线包,目标机零外网可装 · packaging/build-docker.sh → 容器镜像(体验用) 测试 40 套单元测试 + 3 个前端检查脚本(菜单映射 / 离线渲染 / 手机适配), 全部挂在 devtools/sync.sh 的预检链里。 许可:MIT
82 lines
2.7 KiB
Python
82 lines
2.7 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""入口页资源版本号注入(破浏览器缓存)的纯逻辑测试。
|
|
|
|
从 drouter-web.py 抽出 _ASSET_RE / _inject_asset_version 两个片段,
|
|
在隔离命名空间里执行,不导入整个后端(那会连数据库、读证书)。
|
|
"""
|
|
import ast
|
|
import os
|
|
import re
|
|
import sys
|
|
import tempfile
|
|
|
|
SRC = os.path.join(os.path.dirname(__file__), '..', 'backend', 'drouter-web.py')
|
|
code = open(SRC, encoding='utf-8').read()
|
|
tree = ast.parse(code)
|
|
|
|
want = {'_ASSET_RE', '_inject_asset_version'}
|
|
picked = []
|
|
for node in tree.body:
|
|
if isinstance(node, ast.Assign):
|
|
for t in node.targets:
|
|
if isinstance(t, ast.Name) and t.id in want:
|
|
picked.append(node)
|
|
break
|
|
elif isinstance(node, ast.FunctionDef) and node.name in want:
|
|
picked.append(node)
|
|
|
|
found = set()
|
|
for node in picked:
|
|
if isinstance(node, ast.Assign):
|
|
for t in node.targets:
|
|
if isinstance(t, ast.Name):
|
|
found.add(t.id)
|
|
else:
|
|
found.add(node.name)
|
|
|
|
missing = want - found
|
|
assert not missing, '未能从源码抽取:%s' % ', '.join(sorted(missing))
|
|
|
|
# 建一个假 WEB_DIR,里面放两个已知 mtime 的文件
|
|
tmp = tempfile.mkdtemp()
|
|
for name in ('app.js', 'app.css'):
|
|
p = os.path.join(tmp, name)
|
|
open(p, 'w', encoding='utf-8').write('/* %s */' % name)
|
|
os.utime(p, (1_700_000_000, 1_700_000_000))
|
|
|
|
ns = {'os': os, 're': re, 'WEB_DIR': tmp}
|
|
exec(compile(ast.Module(body=picked, type_ignores=[]), SRC, 'exec'), ns)
|
|
inject = ns['_inject_asset_version']
|
|
|
|
HTML = ('<!doctype html>\n'
|
|
'<link rel="icon" href="/logo.svg" type="image/svg+xml">\n'
|
|
'<link rel="stylesheet" href="/app.css">\n'
|
|
'<link rel="stylesheet" href="/theme.css?v=123">\n'
|
|
'<script src="/app.js"></script>\n')
|
|
|
|
out = inject(HTML.encode('utf-8')).decode('utf-8')
|
|
|
|
fails = []
|
|
|
|
|
|
def ck(name, cond, extra=''):
|
|
print(' %-42s %s %s' % (name, 'OK ' if cond else 'FAIL', extra))
|
|
if not cond:
|
|
fails.append(name)
|
|
|
|
|
|
print('资源版本号注入:')
|
|
ck('app.css 追加了 ?v=', '/app.css?v=1700000000' in out)
|
|
ck('app.js 追加了 ?v=', '/app.js?v=1700000000' in out)
|
|
ck('已带查询串的 theme.css 不被改动', '/theme.css?v=123' in out and '/theme.css?v=123?v=' not in out)
|
|
ck('非 js/css 的 logo.svg 不动', '/logo.svg' in out and '/logo.svg?v=' not in out)
|
|
ck('不会重复追加(幂等)',
|
|
inject(out.encode('utf-8')).decode('utf-8').count('/app.js?v=') == 1)
|
|
ck('输出仍是合法 utf-8 且保留 doctype', out.startswith('<!doctype html>'))
|
|
ck('单引号属性也能处理',
|
|
"/app.css?v=" in inject(b"<link href='/app.css'>").decode('utf-8'))
|
|
|
|
print('\n结果:%d 项,失败 %d 项' % (7, len(fails)))
|
|
raise SystemExit(1 if fails else 0)
|