docs: 修复 spec 评审 3 发现的 3 个问题(步骤 3 两段式、domain 结构校验、步骤 4 warning+步骤 5 验证 default)
This commit is contained in:
@@ -62,17 +62,35 @@ def enable_nginx_site(remote_conf: str, domain: str) -> bool:
|
||||
|
||||
| 步骤 | SSH 命令 | 失败处理 | 失败时是否继续 |
|
||||
|---|---|---|---|
|
||||
| 1. 诊断 | `test -e <target> && (test -L <target> && echo "symlink" \|\| (test -d <target> && echo "dir" \|\| echo "file")) \|\| echo "missing"` | 仅诊断,不失败 | — |
|
||||
| 2. 清理 | `case 诊断结果 in symlink) unlink <target> ;; dir) rm -rf <target> ;; file) rm -f <target> ;; missing) ;; esac` | 失败时打印 stderr + 提示"权限可能不足" | **否**(清理失败则终止,不重建链) |
|
||||
| 3. 建链 | `ln -snf {remote_conf} /etc/nginx/sites-enabled/{domain}.conf` | 失败时提示"scp_file 是否成功" | — |
|
||||
| 4. 清默认 | `rm -f /etc/nginx/sites-enabled/default` | 失败时提示"default 文件被占用或权限不足" | — |
|
||||
| 5. 验证 | `readlink /etc/nginx/sites-enabled/{domain}.conf && ls -la /etc/nginx/sites-enabled/` | 失败时打印实际值与期望值 | — |
|
||||
| 1. 诊断 | `test -e <target> && (test -L <target> && echo "symlink" \|\| (test -d <target> && echo "dir" \|\| echo "file")) \|\| echo "missing"` | 仅诊断,不失败(stdout 捕获到 `status` 变量) | — |
|
||||
| 2. 清理 | `case "$status" in symlink) unlink <target> ;; dir) rm -rf <target> ;; file) rm -f <target> ;; missing) ;; esac`(`$status` 是步骤 1 输出,通过 Python f-string 注入 SSH 命令) | 失败时打印 stderr + 提示"权限可能不足" | **否**(清理失败则终止,不重建链) |
|
||||
| 3. 建链 | 两段式:先 `if [ -e <target> ]; then echo "二次检查失败:target 仍存在" >&2; exit 1; fi`,再 `ln -snf {remote_conf} <target>` | 失败时区分:二次检查失败 → 提示"并发进程干扰";ln 自身失败 → 提示"scp_file 是否成功"或"权限不足" | — |
|
||||
| 4. 清默认 | `rm -f /etc/nginx/sites-enabled/default` | 失败时**仅 warning**(不阻塞):打印"default 站点可能仍存在,请在步骤 5 验证时确认" | — |
|
||||
| 5. 验证 | `readlink /etc/nginx/sites-enabled/{domain}.conf && (test ! -e /etc/nginx/sites-enabled/default && echo "default:OK" \|\| echo "default:STILL_EXISTS") && ls -la /etc/nginx/sites-enabled/` | 失败时打印实际值与期望值(含 default 状态) | — |
|
||||
|
||||
**步骤间值传递**(Python 端实现):
|
||||
```python
|
||||
# 步骤 1:执行诊断,stdout 捕获为 status
|
||||
ok, status, err = ssh_command(diagnose_cmd, capture=True)
|
||||
status = status.strip() # 期望值: "symlink" | "dir" | "file" | "missing"
|
||||
|
||||
# 步骤 2:用 f-string 注入 status 到 case 语句
|
||||
cleanup_cmd = f'case "{status}" in symlink) unlink {target} ;; ...) ;; esac'
|
||||
ok, _, err = ssh_command(cleanup_cmd, capture=True)
|
||||
if not ok:
|
||||
log_error("清理步骤失败")
|
||||
return False # 短路
|
||||
```
|
||||
|
||||
**关键决策**:
|
||||
- **步骤 2 用 `test -e/-L/-d/-f` 替代 `ls -ld` 解析**:shell 内置 test 比解析 `ls` 输出更可靠,跨 Unix 工具版本差异小
|
||||
- **步骤 2 失败则短路终止**:清理失败时建链毫无意义(脏状态仍在),立即返回 False 让用户介入
|
||||
- **有效 symlink 走 `unlink` 而非 `rm -f`**:与目录/文件区分对待,避免误删有效链接
|
||||
- **缺失(missing)跳过清理**:直接进建链步骤,是最常见的健康场景
|
||||
- **步骤 3 加 `test ! -e <target>` 二次防护**:极端并发场景(步骤 2 清理后、步骤 3 执行前有其他进程瞬间创建同名项)下,`ln -snf` 仍会 hang;前置 test 检查 + 失败时明确提示"并发进程干扰",避免静默 hang
|
||||
- **步骤 3 用两段式而非 `&& ... ||` 链**:避免 `ln` 自身失败(如权限不足)被误报为"二次检查失败",错误信息更精准
|
||||
- **步骤 4 失败仅 warning 不短路**:default 站点清理是非关键操作(即使存在也不阻塞 symlink 启用),失败时打印 warning 告知"default 站点可能仍存在",由用户在步骤 5 验证时确认
|
||||
- **步骤 5 验证同时检查 default**:除 `readlink` 验证 symlink 外,追加 `test ! -e /etc/nginx/sites-enabled/default` 确认 default 站点已清理;失败时同时打印 symlink 状态和 default 状态
|
||||
|
||||
**超时设置**:每步独立调用 `ssh_command(cmd, timeout=30)`,与现有 `ssh_command()` 默认值一致。**不修改** `ssh_command()` / `run_ssh_args()` 底层,也不需要新增超时参数。30s 对单步 SSH 命令(不涉及大文件传输)足够;如未来出现网络慢问题,可在调用处单独覆盖(如 `ssh_command(cmd, timeout=60)`)。
|
||||
|
||||
@@ -87,21 +105,38 @@ def enable_nginx_site(remote_conf: str, domain: str) -> bool:
|
||||
|
||||
### 变量安全校验
|
||||
|
||||
`enable_nginx_site(remote_conf, domain)` 函数入口处对 `domain` 做基本校验,防止变量污染导致误操作:
|
||||
`enable_nginx_site(remote_conf, domain)` 函数入口处对 `domain` 和 `remote_conf` 都做基本校验,防止变量污染导致误操作:
|
||||
|
||||
```python
|
||||
import re
|
||||
if not domain or not re.match(r'^[a-zA-Z0-9.-]+$', domain):
|
||||
_SAFE_DOMAIN = re.compile(r'^[a-zA-Z0-9.-]+$') # domain 字符集
|
||||
_SAFE_REMOTE_CONF = re.compile(r'^[a-zA-Z0-9./_-]+$') # 路径字符集(允许下划线)
|
||||
|
||||
if not domain or not _SAFE_DOMAIN.match(domain):
|
||||
log_error(f"非法 domain 名称: {domain!r}")
|
||||
return False
|
||||
# domain 额外结构校验:禁止首尾连字符/点、禁止连续点(非法 TLD)
|
||||
if domain.startswith('-') or domain.endswith('-') \
|
||||
or domain.startswith('.') or domain.endswith('.') \
|
||||
or '..' in domain:
|
||||
log_error(f"非法 domain 结构: {domain!r} (首尾连字符/点 或 连续点非法)")
|
||||
return False
|
||||
|
||||
if not remote_conf or not _SAFE_REMOTE_CONF.match(remote_conf):
|
||||
log_error(f"非法 remote_conf 路径: {remote_conf!r}")
|
||||
return False
|
||||
# 路径额外结构校验:禁止连续斜杠(路径注入)、禁止 .. 路径遍历
|
||||
if '//' in remote_conf or '..' in remote_conf:
|
||||
log_error(f"remote_conf 包含可疑路径片段: {remote_conf!r}")
|
||||
return False
|
||||
```
|
||||
|
||||
校验规则:
|
||||
- 非空
|
||||
- 仅包含字母、数字、点、连字符
|
||||
- 不含 `/`、`*`、`;`、`&`、`|`、`$`、空格等 shell 特殊字符
|
||||
- `domain` 非空,仅含字母、数字、点、连字符;**首尾不能是 `-` 或 `.`;不能含 `..` 连续点**
|
||||
- `remote_conf` 非空,仅含字母、数字、`/`、点、连字符、下划线;**不能含 `//` 连续斜杠或 `..` 路径遍历**
|
||||
- 不含 `;`、`&`、`|`、`$`、反引号、空格等 shell 特殊字符
|
||||
|
||||
`remote_conf` 来源固定(f-string 拼接 `f"/etc/nginx/sites-available/{DOMAIN}.conf"`),与 `domain` 一同校验。
|
||||
虽然 `remote_conf` 当前来源固定(f-string 拼接),但**未来重构时**(如改为从配置文件读取)这个校验能防止意外引入危险字符。
|
||||
|
||||
### 错误信息改进
|
||||
|
||||
@@ -149,6 +184,8 @@ if not domain or not re.match(r'^[a-zA-Z0-9.-]+$', domain):
|
||||
3. **目录场景**:手动 `mkdir /etc/nginx/sites-enabled/{domain}.conf` → 期望步骤 2 `rm -rf` 清理 + 步骤 3 成功
|
||||
4. **文件场景**:手动 `touch /etc/nginx/sites-enabled/{domain}.conf` → 期望步骤 2 `rm -f` 清理 + 步骤 3 成功
|
||||
5. **正常 symlink 场景**:手动重建有效 symlink → 期望步骤 2 unlink + 步骤 3 重建
|
||||
6. **权限不足场景**:以非 root 用户运行 deploy.py(或临时 `chmod 000 /etc/nginx/sites-enabled/{domain}.conf` 父目录) → 期望步骤 2 正确报错 + 短路终止,不进入步骤 3
|
||||
7. **并发干扰场景**(极难手动复现,可选):在步骤 2 清理和步骤 3 之间用 `watch` 命令持续创建同名目录 → 期望步骤 3 二次防护触发,提示"并发进程干扰"
|
||||
|
||||
### 集成级
|
||||
|
||||
@@ -167,9 +204,11 @@ if not domain or not re.match(r'^[a-zA-Z0-9.-]+$', domain):
|
||||
|
||||
### 风险
|
||||
|
||||
1. **远程 rm -rf 误删**:仅针对 `/etc/nginx/sites-enabled/{domain}.conf` 单个路径(已通过 `{domain}` 限定),不会影响其他站点
|
||||
2. **步骤 2 误判**:如果 `ls -ld` 输出解析错误,错误地删了文件 → 步骤 3 重新创建,整体仍自愈
|
||||
1. **远程 rm -rf 误删**:仅针对 `/etc/nginx/sites-enabled/{domain}.conf` 单个路径(已通过 `{domain}` 限定 + 变量安全校验),不会影响其他站点
|
||||
2. **步骤 2 误判**:如果 `test -e/-L/-d/-f` 在远程 shell 版本异常或文件系统不支持(如 NFS 边缘情况)时输出不符合预期 → 步骤 3 二次防护 `test ! -e` 仍能检测到残留,整体仍自愈
|
||||
3. **SSH 连接问题掩盖**:如果 SSH 本身不通,新代码会立即在步骤 1 报告 stderr,不会比原代码更差
|
||||
4. **并发进程干扰**:步骤 2 清理和步骤 3 之间有其他进程创建同名目录 → 步骤 3 二次防护触发,提示"并发进程干扰"而非静默 hang
|
||||
5. **变量污染**:通过变量安全校验(domain + remote_conf)防御,即使未来重构时从配置读取也不会引入 shell 注入
|
||||
|
||||
### 回滚
|
||||
|
||||
|
||||
Reference in New Issue
Block a user