安全:
- chpasswd 换行注入可改 root 密码:用户名/密码拒绝换行与控制字符
- 删用户保护从写死两个名字改为 UID<1000 系统账号
- WOL 接口名白名单 + 目标地址 IPv4 校验,去 sh -c 管道
- ACL hosts 改 ipaddress 严格校验;存储挂载点限定 /mnt|/media|/srv
- 文件管理器拒绝清单扩到目录级 + 按文件名拦截(authorized_keys/shadow 等)
- 前端 201 处 value="${...}" 补 esc();href 只放行 http/https;
主题导出注释同时剥 /* 与 */
丢数据/假功能:
- ACL/文件共享/DDNS 三页切走再回来草稿被静默覆盖:新增 PAGE_DIRTY 脏页登记
- LAN 页网关/域名写进 system 死键,改绑 dnsmasq 同源
- UPnP 映射表按钮不再顺手起服务,走只读接口 read:upnpmap
资源:
- PTY 缓冲 8MB 封顶 + 会话信号量 + 后台空闲回收 + 离开页面主动断开
- Web 工作线程 48 封顶、请求体 16MB 封顶、会话 500 封顶改单调时钟、
审计 2 万行裁剪、helpd 并发 6 封顶
- helper 日志 32MB 轮转;快照前查磁盘余量;fs 下载 512MB→20MB
(通道单条响应 32MB,超限只会静默失败)
- 11 处配置写入改原子写;rescue/snapshot 单元补 MemoryMax(三处同步)
性能:
- /api/config 16 次开库 → 1 次 IN 查询
- cleanup 状态扫描 30s 缓存;gzip 缓存加 24MB 字节闸
- 后台标签页心跳暂停、终端轮询降频
杂项:DHCPv6 停用只留注释;租约回收用 dhcp_release(DEPS 可选登记);
VLAN 只删自建接口;公网探测清残留结果文件;快照根白名单;
心跳定时器防重复;keep_days/keep_rows 不吞 0
248 lines
11 KiB
Python
248 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""外置存储设备(USB / Type-C / 雷电)的纯逻辑测试。
|
||
|
||
重点守住「格式化」的三重保护 —— 这是全项目最容易造成不可逆损失的操作:
|
||
1) 系统盘(承载 / /boot /boot/efi 等)禁止格式化;
|
||
2) 已挂载的设备必须先卸载;
|
||
3) 整块盘若已含分区,禁止格式化整盘(应格式化具体分区);
|
||
4) 前端二次确认必须手输设备名,后端还要再校验一次。
|
||
|
||
从 drouter-helper.py 抽取相关函数,lsblk / mkfs / mount 全部换成假的。
|
||
"""
|
||
import ast
|
||
import json
|
||
import os
|
||
import shutil
|
||
import sys
|
||
import tempfile
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
SRC = os.path.join(HERE, '..', 'backend', 'drouter-helper.py')
|
||
code = open(SRC, encoding='utf-8').read()
|
||
tree = ast.parse(code)
|
||
|
||
WANT_FN = ['_blk_sysfs', '_dev_bus', '_walk_blk', '_sysfs_model',
|
||
'_dev_is_system', '_fs_meta', '_fs_tool_ready', '_read_fstab',
|
||
'_backup_fstab', '_safe_mnt_seg', '_mnt_target_error', 'act_storage']
|
||
WANT_AS = ['FS_TYPES', '_CRITICAL_MOUNTS']
|
||
|
||
picked = []
|
||
for node in tree.body:
|
||
if isinstance(node, ast.FunctionDef) and node.name in WANT_FN:
|
||
picked.append(node)
|
||
elif isinstance(node, ast.Assign):
|
||
for t in node.targets:
|
||
if isinstance(t, ast.Name) and t.id in WANT_AS:
|
||
picked.append(node)
|
||
got = set()
|
||
for n in picked:
|
||
if isinstance(n, ast.Assign):
|
||
got.update(t.id for t in n.targets if isinstance(t, ast.Name))
|
||
else:
|
||
got.add(n.name)
|
||
missing = (set(WANT_FN) | set(WANT_AS)) - got
|
||
assert not missing, '未能从源码抽取:%s' % ', '.join(sorted(missing))
|
||
|
||
# ---- 测试替身 -------------------------------------------------------------
|
||
STATE = {'devices': [], 'sh_rc': 0, 'sh_out': '', 'sh_calls': []}
|
||
|
||
|
||
def fake_sh(cmd, timeout=None, **kw):
|
||
STATE['sh_calls'].append(cmd)
|
||
return (STATE['sh_rc'], STATE['sh_out'], '')
|
||
|
||
|
||
def fake_log(*a, **k):
|
||
pass
|
||
|
||
|
||
GEN = tempfile.mkdtemp()
|
||
|
||
def _ok(data=None, msg='操作成功', code='OK'):
|
||
return {'ok': True, 'code': code, 'msg_cn': msg, 'data': data}
|
||
|
||
|
||
def _fail(msg, code='ERR', data=None):
|
||
return {'ok': False, 'code': code, 'msg_cn': msg, 'data': data}
|
||
|
||
|
||
ns = {'os': os, 'json': json, 'shutil': shutil, 'time': __import__('time'),
|
||
're': __import__('re'),
|
||
'sh': fake_sh, 'log': fake_log, 'GEN': GEN,
|
||
'ok': _ok, 'fail': _fail,
|
||
'_storage_device_rows': lambda: STATE['devices'],
|
||
'_hbytes': lambda n: '%d B' % n}
|
||
exec(compile('\n'.join(ast.unparse(n) for n in picked), '<x>', 'exec'), ns)
|
||
|
||
_dev_bus = ns['_dev_bus']
|
||
_dev_is_system = ns['_dev_is_system']
|
||
_fs_meta = ns['_fs_meta']
|
||
_fs_tool_ready = ns['_fs_tool_ready']
|
||
act_storage = ns['act_storage']
|
||
FS_TYPES = ns['FS_TYPES']
|
||
|
||
fails = []
|
||
|
||
|
||
def ck(name, cond, extra=''):
|
||
print(' %-46s %s %s' % (name, 'OK ' if cond else 'FAIL', extra))
|
||
if not cond:
|
||
fails.append(name)
|
||
|
||
|
||
print('=' * 66)
|
||
print('一、连接方式判定(总线类型)')
|
||
ck('sysfs 含 thunderbolt → 雷电',
|
||
_dev_bus('sdb', '/sys/devices/pci0000:00/0000:00:0d.2/thunderbolt/0-1/nvme') ==
|
||
('thunderbolt', '雷电 / USB4'))
|
||
ck('sysfs 含 /usb → USB',
|
||
_dev_bus('sdc', '/sys/devices/pci0000:00/0000:00:14.0/usb2/2-1/2-1:1.0/block/sdc') ==
|
||
('usb', 'USB'))
|
||
ck('sysfs 含 nvme → NVMe',
|
||
_dev_bus('nvme0n1', '/sys/devices/pci0000:00/nvme/nvme0/nvme0n1') ==
|
||
('nvme', 'NVMe'))
|
||
ck('sysfs 含 /ata → SATA',
|
||
_dev_bus('sda', '/sys/devices/pci0000:00/ata1/host0/target0:0:0/0:0:0:0/block/sda') ==
|
||
('sata', 'SATA / SCSI'))
|
||
ck('sysfs 含 virtio → 虚拟磁盘',
|
||
_dev_bus('vda', '/sys/devices/pci0000:00/0000:00:05.0/virtio1/block/vda') ==
|
||
('virtual', '虚拟磁盘'))
|
||
ck('mmcblk 命名 → MMC/TF 卡',
|
||
_dev_bus('mmcblk0', '/sys/devices/platform/mmc0/mmcblk0') ==
|
||
('mmc', 'MMC / TF 卡'))
|
||
ck('未知路径 → unknown', _dev_bus('xxx', '/weird/path')[0] == 'unknown')
|
||
|
||
print('二、系统盘识别(禁止格式化的核心依据)')
|
||
rows = [
|
||
{'name': 'sda', 'parent': '', 'partitions': ['sda1', 'sda2'], 'mountpoint': ''},
|
||
{'name': 'sda1', 'parent': 'sda', 'partitions': [], 'mountpoint': '/boot'},
|
||
{'name': 'sda2', 'parent': 'sda', 'partitions': [], 'mountpoint': '/'},
|
||
{'name': 'sdb', 'parent': '', 'partitions': ['sdb1'], 'mountpoint': ''},
|
||
{'name': 'sdb1', 'parent': 'sdb', 'partitions': [], 'mountpoint': ''},
|
||
]
|
||
ck('sda2(挂载 /)判定为系统盘', _dev_is_system('sda2', rows)[0] is True)
|
||
ck('sda1(挂载 /boot)判定为系统盘', _dev_is_system('sda1', rows)[0] is True)
|
||
ck('整盘 sda(子分区挂 /)也判为系统盘', _dev_is_system('sda', rows)[0] is True)
|
||
ck('外接 sdb1 不是系统盘', _dev_is_system('sdb1', rows)[0] is False)
|
||
|
||
print('三、文件系统元数据')
|
||
ck('FS_TYPES 不少于 7 种', len(FS_TYPES) >= 7, '实际 %d' % len(FS_TYPES))
|
||
ck('exFAT 三平台通用',
|
||
all(_fs_meta('exfat')[k] for k in ('win', 'mac', 'linux')))
|
||
ck('ext4 仅 Linux', _fs_meta('ext4')['linux'] and not _fs_meta('ext4')['win'])
|
||
ck('FAT32 有 4GB 限制说明', '4GB' in _fs_meta('vfat')['note'])
|
||
ck('每个 fs 都有 mkfs / pkg / note',
|
||
all(f.get('mkfs') and f.get('pkg') and f.get('note') for f in FS_TYPES))
|
||
ck('不支持的 fs 返回 None', _fs_meta('reiser9') is None)
|
||
|
||
print('四、格式化工具可用性探测(PATH 里找可执行文件)')
|
||
tmpbin = tempfile.mkdtemp()
|
||
for t in ('mkfs.ext4', 'mkfs.exfat'):
|
||
p = os.path.join(tmpbin, t)
|
||
open(p, 'w').write('#!/bin/sh\n')
|
||
os.chmod(p, 0o755)
|
||
old_path = os.environ['PATH']
|
||
os.environ['PATH'] = tmpbin
|
||
try:
|
||
ck('PATH 里有 mkfs.ext4 → ready', _fs_tool_ready('ext4')[0] is True)
|
||
ck('PATH 里没有 mkfs.ntfs → not ready', _fs_tool_ready('ntfs')[0] is False)
|
||
ck('不可用时给出软件包名', 'ntfs-3g' in _fs_tool_ready('ntfs')[1])
|
||
finally:
|
||
os.environ['PATH'] = old_path
|
||
|
||
print('五、format 的三重保护(最关键)')
|
||
EXT = [{'name': 'sdb1', 'type': 'part', 'fstype': '', 'mounted': False,
|
||
'mountpoint': '', 'parts': []}]
|
||
STATE['devices'] = [
|
||
{'name': 'sda', 'type': 'disk', 'fstype': '', 'mountpoint': '', 'mounted': False,
|
||
'parent': '', 'partitions': ['sda1'], 'has_children': True},
|
||
{'name': 'sda1', 'type': 'part', 'fstype': 'ext4', 'mountpoint': '/',
|
||
'mounted': True, 'parent': 'sda', 'partitions': [], 'has_children': False},
|
||
{'name': 'sdb', 'type': 'disk', 'fstype': '', 'mountpoint': '', 'mounted': False,
|
||
'parent': '', 'partitions': ['sdb1'], 'has_children': True},
|
||
{'name': 'sdb1', 'type': 'part', 'fstype': '', 'mountpoint': '', 'mounted': False,
|
||
'parent': 'sdb', 'partitions': [], 'has_children': False},
|
||
{'name': 'sdc1', 'type': 'part', 'fstype': 'exfat', 'mountpoint': '/mnt/x',
|
||
'mounted': True, 'parent': '', 'partitions': [], 'has_children': False},
|
||
]
|
||
|
||
r = act_storage({'op': 'format', 'name': 'sda1', 'fs': 'ext4', 'confirm': 'sda1'})
|
||
ck('保护1:系统盘拒绝格式化', (not r['ok']) and r['code'] == 'SYSTEM_DISK', r.get('code'))
|
||
|
||
r = act_storage({'op': 'format', 'name': 'sdc1', 'fs': 'ext4', 'confirm': 'sdc1'})
|
||
ck('保护2:已挂载拒绝格式化', (not r['ok']) and r['code'] == 'MOUNTED', r.get('code'))
|
||
|
||
r = act_storage({'op': 'format', 'name': 'sdb', 'fs': 'ext4', 'confirm': 'sdb'})
|
||
ck('保护3:整盘含分区拒绝格式化', (not r['ok']) and r['code'] == 'DISK_HAS_PARTS',
|
||
r.get('code'))
|
||
|
||
r = act_storage({'op': 'format', 'name': 'sdb1', 'fs': 'ext4', 'confirm': 'sdb'})
|
||
ck('保护4:确认名不匹配拒绝', (not r['ok']) and r['code'] == 'NEED_CONFIRM', r.get('code'))
|
||
|
||
r = act_storage({'op': 'format', 'name': 'sdb1', 'fs': 'ufs', 'confirm': 'sdb1'})
|
||
ck('不支持的文件系统被拒', (not r['ok']) and r['code'] == 'BAD_FS', r.get('code'))
|
||
|
||
# 保持 PATH 里有 mkfs 工具,才能测到「mkfs 本身执行失败」,
|
||
# 而不是被「没装工具(NO_TOOL)」提前拦掉、掩盖了真正的分支。
|
||
os.environ['PATH'] = tmpbin
|
||
STATE['sh_calls'] = []
|
||
r = act_storage({'op': 'format', 'name': 'sdb1', 'fs': 'ext4', 'confirm': 'sdb1'})
|
||
ck('条件齐备时真正执行 mkfs(不再拦截)',
|
||
r['ok'] and STATE['sh_calls'] and STATE['sh_calls'][-1][0] == 'mkfs.ext4',
|
||
str(STATE['sh_calls'][-1] if STATE['sh_calls'] else ''))
|
||
|
||
STATE['sh_rc'] = 1
|
||
r = act_storage({'op': 'format', 'name': 'sdb1', 'fs': 'exfat', 'confirm': 'sdb1'})
|
||
ck('mkfs 执行失败时如实报错(不假装成功)',
|
||
(not r['ok']) and r['code'] == 'FORMAT', r.get('code'))
|
||
STATE['sh_rc'] = 0
|
||
os.environ['PATH'] = old_path
|
||
|
||
print('六、挂载 / 卸载')
|
||
STATE['sh_calls'] = []
|
||
r = act_storage({'op': 'mount', 'name': 'sdb1'})
|
||
ck('未格式化的设备不能挂载', (not r['ok']) and r['code'] == 'NOFS', r.get('code'))
|
||
r = act_storage({'op': 'mount', 'name': 'sdc1'})
|
||
ck('已挂载的设备不能重复挂载', (not r['ok']) and r['code'] == 'ALREADY', r.get('code'))
|
||
r = act_storage({'op': 'umount', 'name': 'sdb1'})
|
||
ck('未挂载的设备不能卸载', (not r['ok']) and r['code'] == 'NOTMOUNTED', r.get('code'))
|
||
STATE['sh_rc'] = 1
|
||
r = act_storage({'op': 'umount', 'name': 'sdc1'})
|
||
ck('卸载失败时给出排查命令(lsof/fuser)',
|
||
(not r['ok']) and ('lsof' in r['msg_cn']), r.get('code'))
|
||
STATE['sh_rc'] = 0
|
||
|
||
print('七、fstab 开机自动挂载')
|
||
# 不测真实写入(那会动 /etc/fstab),只测参数校验与重复检测:
|
||
# 这两条最容易出「写进去了但开机起不来」和「同一设备被重复追加两次」。
|
||
r = act_storage({'op': 'fstab_add', 'name': 'sda', 'target': '/mnt/x'})
|
||
ck('无 UUID 的设备不能写 fstab',
|
||
r.get('code') in ('NOUUID', 'NODEV'), r.get('code'))
|
||
r = act_storage({'op': 'fstab_del', 'uuid': ''})
|
||
ck('fstab_del 缺少 UUID 时报错', r.get('code') == 'NOUUID', r.get('code'))
|
||
# 重复检测:把 /etc/fstab 内容里塞进同一 UUID,应当判定为已存在
|
||
ns['_read_fstab'] = lambda: '# test\nUUID=AAAA-1111 /mnt/usb exfat defaults 0 0\n'
|
||
for d in STATE['devices']:
|
||
if d['name'] == 'sdb1':
|
||
d['uuid'] = 'AAAA-1111'
|
||
d['fstype'] = 'exfat'
|
||
r = act_storage({'op': 'fstab_add', 'name': 'sdb1', 'target': '/mnt/usb'})
|
||
ck('同一 UUID 重复添加会被拒绝', r.get('code') == 'DUP', r.get('code'))
|
||
|
||
print('八、前端必须有的要素')
|
||
js = open(os.path.join(HERE, '..', 'web', 'app.js'), encoding='utf-8').read()
|
||
for k, label in [('stgLoad', '读取设备列表'), ('stgFormat', '格式化弹窗'),
|
||
('stg-confirm', '手输设备名确认'), ('stgShare', '一键加入共享'),
|
||
('开机挂载', '开机自动挂载按钮'), ('外置存储设备', '卡片标题')]:
|
||
ck('前端含 %s' % label, k in js)
|
||
ck('格式化弹窗会校验确认名再提交',
|
||
'confirm !== d.name' in js)
|
||
ck('已挂载时格式化按钮禁用', "d.mounted ? 'disabled" in js or 'disabled title="请先卸载再格式化"' in js)
|
||
|
||
print('=' * 66)
|
||
print('结果:失败 %d 项' % len(fails))
|
||
shutil.rmtree(tmpbin, ignore_errors=True)
|
||
shutil.rmtree(GEN, ignore_errors=True)
|
||
sys.exit(1 if fails else 0)
|