From 0a79a6f6318be98d6d11634821d68b5078c0cebd Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Tue, 11 Aug 2026 23:30:45 +0000 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=88=A0=E9=99=A4=E5=B7=B2=E5=BA=9F?= =?UTF-8?q?=E5=BC=83=E7=9A=84=20dnfs=20=E6=A8=A1=E5=9D=97=EF=BC=8C?= =?UTF-8?q?=E7=94=B1=20dnfs2=20(C++20)=20=E5=B9=B3=E6=9B=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _inst.sh | 1 - dnfs/.dockerignore | 18 - dnfs/.gitignore | 112 ------ dnfs/Dockerfile | 48 --- dnfs/LICENSE | 21 - dnfs/README.md | 347 ----------------- dnfs/_run.sh | 5 - dnfs/_set.sh | 7 - dnfs/dnfs.py | 739 ------------------------------------ dnfs/log_client.py | 4 - dnfs/requirements.txt | 7 - dnfs/security_middleware.py | 42 -- 12 files changed, 1351 deletions(-) delete mode 100644 dnfs/.dockerignore delete mode 100644 dnfs/.gitignore delete mode 100644 dnfs/Dockerfile delete mode 100644 dnfs/LICENSE delete mode 100644 dnfs/README.md delete mode 100755 dnfs/_run.sh delete mode 100755 dnfs/_set.sh delete mode 100755 dnfs/dnfs.py delete mode 100644 dnfs/log_client.py delete mode 100644 dnfs/requirements.txt delete mode 100644 dnfs/security_middleware.py diff --git a/_inst.sh b/_inst.sh index 5f32292..0b6a280 100755 --- a/_inst.sh +++ b/_inst.sh @@ -35,7 +35,6 @@ inst_module "gateway" inst_module "waf" # waf 在 gateway 之前编译,部署时由 core 先拉起 waf 再拉起 gateway inst_module "ca" inst_module "fsys" -inst_module "dnfs" inst_module "dnfs2" inst_module "db" inst_module "db2" diff --git a/dnfs/.dockerignore b/dnfs/.dockerignore deleted file mode 100644 index 38c4359..0000000 --- a/dnfs/.dockerignore +++ /dev/null @@ -1,18 +0,0 @@ -.env -.env/ -__pycache__/ -*.pyc -*.pyo -*.pyd -.Python -*.so -.git -.gitignore -*.md -!README.md -.envrc -.venv/ -venv/ -ENV/ -*.bin -.DS_Store diff --git a/dnfs/.gitignore b/dnfs/.gitignore deleted file mode 100644 index 817ce2d..0000000 --- a/dnfs/.gitignore +++ /dev/null @@ -1,112 +0,0 @@ -#prjs -*.db -*.bin -*.build -*.dist -*.onefile-build -nuitka-crash-report.xml - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ diff --git a/dnfs/Dockerfile b/dnfs/Dockerfile deleted file mode 100644 index 7ef7505..0000000 --- a/dnfs/Dockerfile +++ /dev/null @@ -1,48 +0,0 @@ -FROM python:3.11-slim - -LABEL maintainer="qingsong" -LABEL service="dnfs" -LABEL description="Dendrachy Network File-System - 树状网络文件系统" - -# 设置工作目录 -WORKDIR /app - -# 设置环境变量 -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ - PIP_NO_CACHE_DIR=1 \ - PIP_DISABLE_PIP_VERSION_CHECK=1 - -# 安装系统依赖(如果有需要) -RUN apt-get update && apt-get install -y --no-install-recommends \ - && rm -rf /var/lib/apt/lists/* - -# 先复制pymsg共享库并安装 -COPY pymsg/ /app/pymsg/ -RUN pip install /app/pymsg/ - -# 复制requirements并安装依赖 -COPY dnfs/requirements.txt /app/ -RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple - -# 移除flask_sqlalchemy遗留依赖(已不使用) -RUN pip uninstall -y flask_sqlalchemy 2>/dev/null || true - -# 复制应用代码 -COPY dnfs/ /app/dnfs/ - -# 创建非root用户运行(安全最佳实践) -RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app -USER appuser - -WORKDIR /app/dnfs - -# 暴露端口 -EXPOSE 8006 - -# 健康检查 -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8006/get_notes', data=b'{\"pid\":0}', timeout=3)" || exit 1 - -# 启动服务 -CMD ["python", "dnfs.py"] diff --git a/dnfs/LICENSE b/dnfs/LICENSE deleted file mode 100644 index c58cae3..0000000 --- a/dnfs/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 卿松 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/dnfs/README.md b/dnfs/README.md deleted file mode 100644 index f7550d3..0000000 --- a/dnfs/README.md +++ /dev/null @@ -1,347 +0,0 @@ -# dnfs — 树状网络文件系统 - -> **已废弃**:生产已由 [dnfs2](../dnfs2/)(C++20 重写)平替,不再部署;本 README 为历史说明。 - -Dendrachy Network File-System,基于父子关系(pid)组织的树状节点服务。 - -监听 `127.0.0.1:8006`,提供节点的增删改查,支持以 Base64 编码形式在 `content` 中存储文件数据。所有写操作需携带由 gateway 签发的 Token。 - -支持**特殊节点**(管理员专属):`content` 为信封 JSON 时,前端按类型渲染为链接卡片 / 可运行网页 / 图片 / 文件下载,详见[特殊节点](#特殊节点管理员专属)。 - -## 架构 - -``` -客户端 (static/dnfs.html) - │ Bearer Token - ▼ - dnfs :8006 (Flask + Waitress) - │ - ├──► confs :8010 启动时拉取配置(dnfs.ini,强依赖,retry_on_refused 等待就绪) - ├──► db :8005 数据持久化(pymsg TCP RPC,execute 路由) - └──► ai_review :8007 内容审核(HTTP POST /review,仅 add_note/update_note 时调用) -``` - -- 启动时通过 pymsg 从 confs 拉取 `dnfs.ini` 配置,拉取失败直接退出(强依赖);使用 `retry_on_refused=True` 自动等待 confs 就绪。 -- 不直连数据库,所有数据操作通过 pymsg TCP RPC 调用 `db` 服务(`_route: "execute"`),规避 SQL 注入风险。 -- 新增/更新节点前调用 `ai_review` 服务进行内容审核(HTTP POST `{content: "..."}`),审核不通过则拒绝写入。 -- 日志通过 pymsg `AsyncRpcQueue` 异步批量写入 db 的 `Logs` 表,fail-safe 不阻塞业务。 -- 通常部署在 gateway/WAF 之后(`Proxy.opened` 白名单中),由 gateway 完成 Token RSA 验签后转发。 - -## 数据模型 - -对应 `db` 服务中的 `Notes` 表(见 [db2/scripts/create_sqlite3.sql](../db2/scripts/create_sqlite3.sql)): - -| 字段 | 类型 | 说明 | -|------|------|------| -| id | INTEGER PK | 节点唯一标识 | -| pid | INTEGER NOT NULL | 父节点 id,0 表示根 | -| owner | VARCHAR(128) | 创建者账号(来自 Token payload 的 `user` 字段,默认 `'Unknow'`) | -| title | VARCHAR(128) NOT NULL | 节点标题 | -| content | TEXT NOT NULL | 节点内容(可含 Base64 文件数据) | - -## 鉴权与所有权 - -写操作(`add_note` / `delete_note` / `update_note`)需在请求头携带: - -``` -Authorization: Bearer _ -``` - -- Token 由 gateway 在 `/login` 签发;dnfs 通过 [dnfs.py:312](dnfs.py#L312) `decode_token` 将 Token 转发至 gateway `/whoami` 接口,由 gateway 完成 **RSA 签名验证 + 过期检查**(不再本地解码验签);gateway 不可达时按 **fail-closed** 返回 503,验签失败返回 None。 -- payload 中取 `user` 字段作为操作者,写入 `owner`。 -- `delete_note` 与 `update_note` 校验所有权: - - 节点 `owner` 必须等于当前 `user`,否则 `403`; - - **例外**:管理员(Token payload `is_admin=true`,由 gateway 按 `[Admin] users` 白名单签发)可删除/更新任意节点。 -- `get_notes` 不需要鉴权。 - -## 特殊节点(管理员专属) - -`content` 为以下信封 JSON 时即为特殊节点,前端 `dnfs.js` 按 `_sp` 类型差异化渲染;普通 Markdown 内容不受影响: - -| `_sp` | 信封字段 | 前端表现 | -|-------|---------|---------| -| `link` | `url`(http/https,≤2048)、`note?` | favicon 图标(经 static `/api/favicon` 代理拉取)+ 可点击链接 | -| `js` | `code`(完整 HTML/CSS/JS 文档,≤512KB)、`note?` | "审计源码"(纯文本展示)+ "运行"(解析完整 HTML 文档注入当前页面:style 注入 head、script 在全局作用域执行可加特效、body 渲染到节点下方;console 输出捕获到下方)按钮 | -| `image` | `name`、`data`(`data:image/` DataURL,≤3.5M 字符)、`note?` | 图片直接显示 | -| `file` | `name`、`mime`、`data`(Base64,≤3.5M 字符)、`note?` | 下载按钮(Base64→Blob→下载) | - -规则: - -- **仅管理员可发布/更新**(`add_note` / `update_note` 中按 `is_admin` 强制,非管理员返回 403);前端发布面板也仅对管理员显示。 -- AI 内容审核剔除 Base64 负载(体量大且无审核意义):`link` 审 `title+url+note`,`js` 审 `title+code 前 4000 字符+note`,`image`/`file` 审 `title+name+note`。 -- `{"_sp"` 开头但 JSON 非法的内容会被拒绝发布(400),防止半截信封流入。 - -## 接口文档 - -所有接口均为 `POST`,`Content-Type: application/json`。 - ---- - -### POST /get_notes - -获取指定父节点下的子节点列表。**无需鉴权**。 - -**Request** - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| pid | int | 否 | 父节点 id,0 表示根,缺省默认 0 | - -```bash -curl -X POST http://localhost:8006/get_notes \ - -H 'Content-Type: application/json' \ - -d '{"pid": 0}' -``` - -**Response 200** — 直接返回节点数组: - -```json -[ - {"id": 1, "pid": 0, "owner": "admin", "title": "根节点", "content": "hello"}, - {"id": 2, "pid": 0, "owner": "alice", "title": "另一个", "content": "world"} -] -``` - -空目录返回 `[]`。 - ---- - -### POST /add_note - -添加新节点。流程:校验参数 → 校验 Token → 校验父节点存在 → AI 内容审核 → 写入数据库(含 owner)。 - -**Request** - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| pid | int | 是 | 父节点 id,0 为根,非 0 时校验父节点是否存在 | -| title | str | 是 | 节点标题,不可为空 | -| content | str | 是 | 节点内容,不可为空(可含 Base64 文件数据) | - -请求头需带 `Authorization: Bearer `。 - -```bash -curl -X POST http://localhost:8006/add_note \ - -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer ' \ - -d '{"pid": 0, "title": "新节点", "content": "节点内容"}' -``` - -**响应** - -| 状态码 | 场景 | 响应体 | -|--------|------|--------| -| 200 | 成功 | `{"success": true}` | -| 400 | 缺少必填字段 | `{"error": "Pid, title and content are required"}` | -| 400 | 父节点不存在(pid≠0) | `{"error": "the new node need a parent"}` | -| 400 | AI 内容审核不通过 | `{"error": "review failed"}` | -| 400 | 数据库写入失败 | `{"success": false}` | -| 401 | 未携带 Token 或 Token 解析失败 | `{"error": "Warning: no Authorization"}` | - ---- - -### POST /delete_note - -删除指定节点。流程:校验参数 → 校验 Token → 检查子节点 → 校验所有权 → 删除。 - -存在子节点时拒绝删除(保护树结构完整性)。**admin 可删除任意节点**,普通用户仅能删除自己拥有的节点。 - -**Request** - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| id | int | 是 | 要删除的节点 id | - -请求头需带 `Authorization: Bearer `。 - -```bash -curl -X POST http://localhost:8006/delete_note \ - -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer ' \ - -d '{"id": 1}' -``` - -**响应** - -| 状态码 | 场景 | 响应体 | -|--------|------|--------| -| 200 | 成功 | `{"success": true}` | -| 400 | 缺少 id | `{"error": "Note ID is required"}` | -| 400 | 存在子节点 | `{"error": "this node has a child "}` | -| 400 | 数据库删除失败 | `{"success": false}` | -| 401 | 未携带 Token | `{"error": "Warning: no Authorization"}` | -| 403 | 非本人节点且非 admin | `{"error": "It's not your note"}` | - ---- - -### POST /update_note - -更新节点的 pid / title / content(pid 变更可用于迁移节点)。流程:校验参数 → 校验 Token → 校验所有权 → AI 内容审核 → 更新。 - -仅节点 owner 可更新;管理员(Token payload `is_admin`)豁免,可更新任意节点。 - -**Request** - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| id | int | 是 | 要更新的节点 id | -| pid | int | 是 | 新的父节点 id(迁移时改变此值) | -| title | str | 是 | 新标题,不可为空 | -| content | str | 是 | 新内容,不可为空 | - -请求头需带 `Authorization: Bearer `。 - -```bash -curl -X POST http://localhost:8006/update_note \ - -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer ' \ - -d '{"id": 1, "pid": 2, "title": "新标题", "content": "新内容"}' -``` - -**响应** - -| 状态码 | 场景 | 响应体 | -|--------|------|--------| -| 200 | 成功 | `{"success": true}` | -| 400 | 缺少必填字段 | `{"error": "Invalid param"}` | -| 400 | AI 内容审核不通过 | `{"error": "review failed"}` | -| 400 | 数据库更新失败 | `{"success": false}` | -| 401 | 未携带 Token | `{"error": "Warning: no Authorization"}` | -| 403 | 非本人节点 | `{"error": "It's not your note"}` | - ---- - -## 安全特性 - -- **安全响应头**([security_middleware.py](security_middleware.py)):`X-Frame-Options: SAMEORIGIN`、`X-XSS-Protection`、`X-Content-Type-Options: nosniff`、`Referrer-Policy: strict-origin-when-cross-origin`、`Permissions-Policy`(禁用摄像头/麦克风/地理位置)、CSP(`default-src 'self'` 等),并移除 `Server` 头。 -- **Token 鉴权**:写操作强制校验,payload 取 `user` 字段。 -- **所有权隔离**:普通用户只能改/删自己的节点。 -- **特殊节点管控**:link/js/image/file 类型有格式校验与大小限制(URL 前缀、文件名长度、内容大小上限)。 -- **内容审核**:写入与更新前调用 `ai_review`,非法内容会被拒绝(fail-closed 设计,见已知问题)。 -- **异步日志**:通过 pymsg `AsyncRpcQueue` 异步批量写日志,fail-safe 不阻塞业务线程。 -- **非 root 容器化运行**:Dockerfile 中使用 appuser 非特权用户运行服务。 - -## 配置 - -dnfs 已接入 confs 统一配置服务,confs 是 dnfs 启动的**强依赖**: - -1. **confs 统一配置服务** — 必须成功从 confs 拉取 `dnfs.ini`,否则 dnfs 启动失败直接退出(exit code 1) -2. **环境变量覆盖**(最高优先级)— Docker/部署环境通过环境变量覆盖 confs 中的值 - -### 配置文件 - -集中托管在 `confs/configs/dnfs.ini`(已删除,配置经 CLI 传递),由 confs 服务分发: - -| Section | Key | 说明 | -|---------|-----|------| -| `[server]` | `host` | 监听地址 | -| `[server]` | `port` | 监听端口 | -| `[database]` | `host` | db 服务地址 | -| `[database]` | `port` | db 服务端口 | -| `[database]` | `timeout` | RPC 调用超时(秒) | -| `[ai_review]` | `url` | AI 审核接口地址 | -| `[ai_review]` | `timeout` | AI 审核请求超时(秒) | - -### 引导配置(confs 连接地址) - -confs 自身的连接地址不通过 confs 获取(避免循环依赖),通过以下方式确定(优先级从高到低): -- 环境变量 `CONFS_HOST` / `CONFS_PORT` -- 默认值 `127.0.0.1:8010` - -### 环境变量(最高优先级,覆盖 confs 配置值) - -环境变量覆盖配置文件值,容器化部署时通过 docker-compose.yml 传入服务名: - -| 变量 | 容器内值 | 说明 | -|------|---------|------| -| `LISTEN_ADDR` | `0.0.0.0` | 监听地址(容器内需绑定所有网卡) | -| `LISTEN_PORT` | `8006` | 监听端口 | -| `DB_HOST` | `db` | db 服务主机名(docker-compose 服务名) | -| `DB_PORT` | `8005` | db 服务端口 | -| `REVIEW_URL` | `http://ai_review:8007/review` | ai_review 服务地址 | -| `CONFS_HOST` | `confs` | confs 服务主机名(引导配置) | -| `CONFS_PORT` | `8010` | confs 服务端口(引导配置) | - -### 启动顺序 - -dnfs 通过 pymsg `retry_on_refused=True` 自动阻塞等待 confs 就绪,**拉取配置阶段不严格要求启动顺序**: -- confs 先启动 → dnfs 立即拉取配置 -- dnfs 先启动 → pymsg 自动重试(每秒一次,每 5 秒打印等待提示),confs 启动后自动连接成功 -- 配置完整性校验:若 dnfs.ini 中缺少必需的 section/key 且无对应环境变量覆盖,仍然报错退出 -- 启动成功时控制台打印:`配置来源: confs (dnfs.ini @ host:port)` - -> 注意:dnfs 对 db/ai_review 的 RPC 调用是运行时按需连接(首次请求时才建立 TCP),db/ai_review 未就绪时相关 API 返回 502 但进程不退出;待依赖服务启动后自动恢复。 - -## 前端 - -前端页面由 `static` 服务承载,不在 dnfs 模块内: - -``` -static/public/dnfs.html 页面结构 -static/public/css/ 样式(index.css / aibox.css 等) -static/public/js/dnfs.js 交互逻辑 -``` - -功能:节点树浏览、新增/编辑/删除节点、文件 Base64 上传与下载、节点迁移(移动到其他父节点下)。 - -> 注:首页路由 `@app.route('/')` 和 `render_template` 相关代码已注释,dnfs 仅提供 JSON API,不直接提供 HTML 页面。 - -## 依赖 - -``` -flask -flask-cors -waitress -pymsg # 内部 RPC 通信库(../pymsg/) -``` - -运行时需要 `confs`(:8010,启动强依赖)、`db`(:8005)和 `ai_review`(:8007)服务可达;对外访问需经 gateway 转发并验签。 - -> `requirements.txt` 中曾列有 `flask_sqlalchemy`,Dockerfile 中已添加 `pip uninstall -y flask_sqlalchemy` 清理遗留依赖;代码中的 SQLAlchemy 直连逻辑已全部注释,通过 pymsg RPC 调用 db 服务。 - -## 运行 - -```bash -bash _set.sh # 创建 venv + 安装依赖 -bash _run.sh # 启动服务(waitress) -``` - -## 已知问题与风险 - -> 状态基准:2026-08-01 审计 + 2026-08-03 逐条代码验证。 - -### 🔴 P0 — 严重问题 - -1. **Token 无 RSA 签名验证,可伪造任意用户身份(含管理员)** — ✅ 已修复 - - 原 `decode_token()` 本地 base64 解码 payload、跳过 RSA 验签(`# FIXME: 验证数字签名`)。 - - 现已重写为转发 gateway `/whoami` 完成验签 + 过期检查([dnfs.py:312-351](dnfs.py#L312-L351)),gateway 不可达时 fail-closed 返回 503。 - -2. **`review_content()` 错误处理 / fail-closed** — ⚠️ 基本修复 - - 已实现 fail-closed:HTTPError/URLError/JSONDecodeError 等异常路径均拒绝请求(写入/更新被拒、负载不送审)。 - - 遗留:函数返回类型不一致(成功返回 `bool`,而 `_validate_sp` 等期望 dict),建议代码清理(审计 P2-1)。 - -3. **`delete_note` / `update_note` 缺少 `@_handle_exp` 装饰器** — ✅ 已修复 - - 现两函数均已加 `@_handle_exp`([dnfs.py:469](dnfs.py#L469)、[dnfs.py:572](dnfs.py#L572)),异常统一返回结构化 JSON,不再出 Flask 默认 500 页。 - -4. **有子节点的父节点可能被删除(树结构完整性保护缺陷)** — ❌ 未修复 - - **数据库层缺失外键约束**: Notes 表 `pid` 列未定义 `FOREIGN KEY ... ON DELETE RESTRICT`([db2/scripts/create_sqlite3.sql](../db2/scripts/create_sqlite3.sql))。 - - **应用层 TOCTOU 竞态**: 子节点查询(SELECT)与删除(DELETE)是两个独立 RPC,无事务保护。 - - **错误处理缺陷**: 多处直接访问 `notes['data']` 而未先确认 `success`(如 [dnfs.py:387](dnfs.py#L387)、[L519](dnfs.py#L519)),`success=False` 时可能抛 KeyError。 - -### 🟠 P1 — 高危问题 - -5. **Token 无过期校验** — ✅ 已修复(gateway `/whoami` 基于 `created_at` + `keep_time` 统一校验,dnfs 不再自行处理) -6. **请求体大小无限制** — ❌ 未修复([dnfs.py:723](dnfs.py#L723) `serve(app, ...)` 未配置 `max_request_body_size`,建议设 10MB 与 WAF 对齐) -7. **`log_client.py` 配置与主模块不一致** — ❌ 未修复([log_client.py:32-33](log_client.py#L32-L33) 直接读 `DB_HOST`/`DB_PORT` 环境变量,不拉取 confs) -8. **`update_note` 修改 pid 时未验证目标父节点存在** — ❌ 未修复([update_note](dnfs.py#L573) 直接更新,不检查 pid≠0 时父节点是否存在) -9. **db RPC 客户端未启用启动等待** — ❌ 未修复([dnfs.py:133](dnfs.py#L133) `_rpc = RpcClient(...)` 无 `retry_on_refused=True`) -10. **`delete_note`/`update_note` 中 `decode_token` 无 try-except** — ✅ 基本解决(两函数均由 `@_handle_exp` 统一捕获异常,不再裸抛 500) -11. **`note_id` 缺少强制类型转换** — ❌ 未修复([dnfs.py:474](dnfs.py#L474)、[L577](dnfs.py#L577) 仅类型提示,依赖 SQLite 类型亲和性隐式匹配) - -### 🟡 P2 — 中危问题 - -12. **错误响应泄露内部信息** — 多处错误消息拼接 `request.remote_addr` 和用户名返回给客户端,应仅记录到服务端日志 -13. **CORS 配置过于宽松** — `CORS(app)` 默认允许所有来源(`*`),建议限制为实际前端域名 -14. **`_call_dbv2` 返回值访问前未统一检查 success** — 多处直接访问 `notes['data']` 而不先确认 `success==True` -15. **`pid` 参数判断逻辑不严谨** — `if not pid: pid = 0` 会将空字符串/None 等非法输入默默转为 0,掩盖参数错误 -16. **遗留注释代码** — 文件中有大量注释掉的旧版 SQLAlchemy 代码和已废弃接口,影响可读性 diff --git a/dnfs/_run.sh b/dnfs/_run.sh deleted file mode 100755 index 1eb97fd..0000000 --- a/dnfs/_run.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -cd "$(dirname "$0")" || exit -source ./.env/bin/activate -#exec ./dnfs.bin -exec python3 dnfs.py diff --git a/dnfs/_set.sh b/dnfs/_set.sh deleted file mode 100755 index bd6a31d..0000000 --- a/dnfs/_set.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -cd "$(dirname "$0")" || exit -python3 -m venv .env -#sudo chmod +x .env/bin/activate -source .env/bin/activate -pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple -#nuitka --standalone dnfs.py diff --git a/dnfs/dnfs.py b/dnfs/dnfs.py deleted file mode 100755 index 4e5d8f7..0000000 --- a/dnfs/dnfs.py +++ /dev/null @@ -1,739 +0,0 @@ -#!/usr/bin/env python3 - -''' - -Dendrachy Network File-System (dnfs) - -树状网络文件系统 - - - -可能加入数据库加密功能。 - -''' - -from flask import Flask, request, jsonify, render_template -from flask_cors import CORS -#from flask_sqlalchemy import SQLAlchemy # 使用pymsg模块进行通信 -from waitress import serve -import os -import sys -import json -import configparser -import urllib.request -import urllib.error -import functools -from typing import Dict, Any -from security_middleware import apply_security_headers -from log_client import logs - -# pymsg 导入 -# 我们有在依赖列表中加入pymsg -# 这个可以省略也许 -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from pymsg import RpcClient, RpcError # noqa: E402 - -# ==================== 配置加载(两层:confs 强依赖 → 环境变量覆盖) ==================== -# confs 是 dnfs 启动的强依赖:必须成功从 confs 拉取 dnfs.ini,否则直接退出。 -# 环境变量优先级最高(Docker 环境使用服务名 db/ai_review 而非 127.0.0.1)。 -# confs 自身的连接地址通过引导配置确定:环境变量 CONFS_HOST/CONFS_PORT 优先, -# 缺省使用 127.0.0.1:8010(与其他服务一致的约定)。 - -# 引导配置:confs 连接地址(不依赖 confs 本身) -_confs_host = os.environ.get('CONFS_HOST', '127.0.0.1') -_confs_port = int(os.environ.get('CONFS_PORT', '8010')) - -# 白名单结构:_merge_ini 仅覆盖此处列出的 section/key,防止意外字段 -_dnfs_config = { - 'server': {'host': '', 'port': ''}, - 'database': {'host': '', 'port': '', 'timeout': ''}, - 'ai_review': {'url': '', 'timeout': ''}, - 'gateway': {'url': '', 'timeout': ''}, -} - - -def _merge_ini(text): - """把 ini 文本合并进 _dnfs_config(仅覆盖已知的 section/key,白名单合并)。""" - ini = configparser.ConfigParser(interpolation=None) - ini.read_string(text) - for section in _dnfs_config: - if ini.has_section(section): - for key, value in ini[section].items(): - if key in _dnfs_config[section]: - _dnfs_config[section][key] = value - - -def _fetch_from_confs(): - """从 confs 统一配置服务拉取 dnfs.ini 全文。 - - 使用 retry_on_refused=True:confs 未就绪时 pymsg 自动阻塞重试(每秒一次, - 每 5 秒打印一次等待提示),直到 confs 启动后自动连接成功。 - RpcError(如配置文件不存在)仍直接报错退出——这是配置错误而非启动时序问题。 - """ - try: - data = RpcClient(_confs_host, _confs_port, 'dnfs', timeout=5, - retry_on_refused=True).call('get', {'name': 'dnfs.ini'}) - except RpcError as e: - print(f'[dnfs] 致命错误: 从 confs 获取 dnfs.ini 失败: {e}', - file=sys.stderr) - sys.exit(1) - return data.get('content') - - -_confs_text = _fetch_from_confs() -_merge_ini(_confs_text) - -# 环境变量覆盖(优先级最高,Docker 环境需要) -if os.environ.get('LISTEN_ADDR'): - _dnfs_config['server']['host'] = os.environ['LISTEN_ADDR'] -if os.environ.get('LISTEN_PORT'): - _dnfs_config['server']['port'] = os.environ['LISTEN_PORT'] -if os.environ.get('DB_HOST'): - _dnfs_config['database']['host'] = os.environ['DB_HOST'] -if os.environ.get('DB_PORT'): - _dnfs_config['database']['port'] = os.environ['DB_PORT'] -if os.environ.get('REVIEW_URL'): - _dnfs_config['ai_review']['url'] = os.environ['REVIEW_URL'] -if os.environ.get('GATEWAY_URL'): - _dnfs_config['gateway']['url'] = os.environ['GATEWAY_URL'] - -# 配置完整性校验 -_required = { - 'server': ['host', 'port'], - 'database': ['host', 'port', 'timeout'], - 'ai_review': ['url', 'timeout'], - 'gateway': ['url', 'timeout'], -} -for section, keys in _required.items(): - for key in keys: - if not _dnfs_config[section][key]: - print(f'[dnfs] 致命错误: 配置项 [{section}] {key} 缺失(dnfs.ini 中未配置且无环境变量覆盖)', - file=sys.stderr) - sys.exit(1) - -# 提取配置常量 -_DB_TIMEOUT = int(_dnfs_config['database']['timeout']) -_DBV2_ADDR = _dnfs_config['database']['host'] -_DBV2_PORT = int(_dnfs_config['database']['port']) -_REVIEW_SERVER_URL = _dnfs_config['ai_review']['url'] -_REVIEW_TIMEOUT = int(_dnfs_config['ai_review']['timeout']) -_GATEWAY_URL = _dnfs_config['gateway']['url'] -_GATEWAY_TIMEOUT = int(_dnfs_config['gateway']['timeout']) -_ADDR = _dnfs_config['server']['host'] -_PORT = int(_dnfs_config['server']['port']) - -_config_source = f'confs (dnfs.ini @ {_confs_host}:{_confs_port})' -print(f'[dnfs] 配置来源: {_config_source}') -print(f'[dnfs] Token 验签: gateway {_GATEWAY_URL}') - -# 管理员判定已上移至 gateway:登录时命中 [Admin] users 白名单即在 Token payload -# 写入 is_admin=true,本服务通过 info.get('is_admin') 读取,不再本地维护名单。 -# 旧 Token 无此字段,按非管理员处理(安全方向)。 - -_rpc = RpcClient(_DBV2_ADDR, _DBV2_PORT, "dnfs") - - -def _call_dbv2(payload: dict, timeout: int = None) -> dict: - """通过 pymsg RPC 调用 dbv2 数据库服务(自动注入 ``_route: "execute"``)。 - - 返回完整响应 dict(``{"success": ..., "data"/"error": ...}``), - 与历史行为一致,调用方无需改动。服务端业务错误返回 success=False; - 网络层故障抛 ConnectionError。 - """ - if timeout is None: - timeout = _DB_TIMEOUT - try: - data = _rpc.call("execute", payload, timeout=timeout) - return {"success": True, "data": data} - except RpcError as e: - return {"success": False, "error": str(e)} - except Exception as e: - raise ConnectionError( - f"dbv2 调用失败 ({_DBV2_ADDR}:{_DBV2_PORT}): {e}" - ) from None - -# AI审核服务地址 -def review_content(content: str, timeout: int = None) -> Dict[str, Any]: - """ - 调用 AI 内容审核服务的 /review 接口,对给定文本进行审核。 - - Args: - content: 待审核的文本内容,不可为空。 - timeout: 请求超时时间(秒),默认使用配置值。 - - Returns: - 字典,包含接口返回的字段,例如: - - 成功时:{"invalid": "true"} 或 {"invalid": "false"} - - API 错误时:{"invalid": "false", "error": "api_error", "detail": "..."} - - 解析失败时:{"invalid": "false", "error": "parse_failed", "raw": "..."} - - 请求参数错误时:{"error": "content is required"} 等 - - 网络或异常时:{"error": "request_failed", "detail": "..."} - """ - if timeout is None: - timeout = _REVIEW_TIMEOUT - url = _REVIEW_SERVER_URL - payload = {"content": content} - json_data = json.dumps(payload).encode("utf-8") - - req = urllib.request.Request( - url, - data=json_data, - headers={"Content-Type": "application/json"}, - method="POST" - ) - - try: - with urllib.request.urlopen(req, timeout=timeout) as resp: - resp_data = resp.read().decode("utf-8") - data = json.loads(resp_data) - # 兼容布尔/字符串两种返回类型:仅明确判定安全才放行,其余一律拦截(fail-closed) - return data.get('invalid') in (False, 'false') - - except urllib.error.HTTPError as e: - # 服务端返回 4xx/5xx 错误时,尝试解析错误正文 - error_body = e.read().decode("utf-8") - try: - return json.loads(error_body) - except json.JSONDecodeError as e: - raise e - except urllib.error.URLError as e: - raise e - except Exception as e: - raise e - -class DnfsExp(Exception): - '''dnfs异常类''' - def __init__(self, msg: str, code: int): - super().__init__(self, msg) - self.status = code - def get_status(self) -> int: - return self.status - -# ==================== 特殊节点(管理员专属) ==================== -# 信封格式:content 为 JSON {"_sp": "link|js|image|file", ...},前端识别后差异化渲染; -# 普通 Markdown 内容不受影响。仅管理员可发布(Token payload is_admin)。 -_SP_TYPES = ('link', 'js', 'image', 'file') -_SP_MAX_URL = 2048 # link 节点 url 长度上限 -_SP_MAX_CODE = 512 * 1024 # js 节点代码长度上限(字符) -_SP_MAX_DATA = 3500 * 1024 # image/file 节点 base64 长度上限(字符,约 2.6MB 原始数据) -_SP_REVIEW_CODE_PREFIX = 4000 # js 节点送审的代码前缀长度 - - -def _parse_sp(content): - """识别特殊节点信封:合法信封返回 dict;普通内容返回 None; - 以 {"_sp" 开头但 JSON 非法视为格式错误(拒绝发布,防半截信封流入)。""" - if not isinstance(content, str): - return None - s = content.lstrip() - if not s.startswith('{"_sp"'): - return None - try: - obj = json.loads(s) - except Exception: - raise DnfsExp('特殊节点格式非法(JSON 解析失败)', 400) - if isinstance(obj, dict) and obj.get('_sp') in _SP_TYPES: - return obj - return None - - -def _validate_sp(sp): - """校验特殊节点信封字段与大小上限,非法抛 DnfsExp。""" - t = sp['_sp'] - if t == 'link': - url = sp.get('url', '') - if not isinstance(url, str) or not url.startswith(('http://', 'https://')): - raise DnfsExp('链接节点必须是 http/https URL', 400) - if len(url) > _SP_MAX_URL: - raise DnfsExp('链接过长', 400) - elif t == 'js': - code = sp.get('code', '') - if not isinstance(code, str) or not code.strip(): - raise DnfsExp('前端代码不能为空', 400) - if len(code) > _SP_MAX_CODE: - raise DnfsExp('前端代码过大(上限 512KB)', 400) - elif t == 'image': - data = sp.get('data', '') - if not isinstance(data, str) or not data.startswith('data:image/'): - raise DnfsExp('图片节点 data 必须是 data:image/ 开头的 DataURL', 400) - if len(data) > _SP_MAX_DATA: - raise DnfsExp('图片过大(上限约 2.6MB)', 400) - elif t == 'file': - name = sp.get('name', '') - data = sp.get('data', '') - if not isinstance(name, str) or not name.strip(): - raise DnfsExp('文件节点缺少文件名', 400) - if len(name) > 255: - raise DnfsExp('文件名过长', 400) - if not isinstance(data, str) or not data: - raise DnfsExp('文件节点缺少数据', 400) - if len(data) > _SP_MAX_DATA: - raise DnfsExp('文件过大(上限约 2.6MB)', 400) - - -def _sp_review_text(title, sp): - """特殊节点的 AI 审核文本:剔除 base64 负载(体量大且无审核意义),仅保留文本字段。""" - parts = [title or ''] - note = sp.get('note') - if isinstance(note, str) and note.strip(): - parts.append(note) - t = sp['_sp'] - if t == 'link': - parts.append(str(sp.get('url', ''))) - elif t == 'js': - parts.append(str(sp.get('code', ''))[:_SP_REVIEW_CODE_PREFIX]) - else: # image / file - parts.append(str(sp.get('name', ''))) - return '\n'.join(parts) - -# 这个也许不用这么复杂 大不了每个异常点我们抛出DnfsExp类 -#class InvalidRequest(DnfsExp): -# '''非法参数异常''' -# def __init__(self, msg: str): -# super().__init__(msg) - -def _handle_exp(func): - '''处理视图函数中抛出的异常,并在日志模块中进行记录''' - @functools.wraps(func) - def wrapper(): - try: return func() - except DnfsExp as e: - logs.warning('dnfs', str(e), extra={}) - return jsonify({'error': str(e)}), e.get_status() - except Exception as e: - logs.warning('dnfs', str(e), extra={}) - return jsonify({'error': str(e)}), 500 - return wrapper - -app = Flask(__name__) -apply_security_headers(app) -CORS(app) - - -# [SECURITY-AUDIT] P0-1 / P1-5 —— 已修复(审计结论见 docs/security/dnfs_SECURITY_AUDIT_20260801.md) -# 原问题: -# · P0-1 Token 无 RSA 签名验证,攻击者可伪造任意身份(含管理员)。 -# · P1-5 Token 无过期校验,被盗 Token 可长期复用。 -# 修复方式(本项目统一安全决策):本服务不持有密钥,Token 校验全部上移至 gateway。 -# decode_token 转发 gateway /whoami,由 gateway 完成 RSA 验签 + 基于 -# created_at+keep_time 的过期校验;gateway 不可达 → fail-closed 抛 503, -# 验签失败/未登录 → 返回 None(调用方转 401)。is_admin 由 gateway 基于 -# [Admin] 白名单在 Token payload 写入,本服务只读,不再本地判定。 -def decode_token(token: str): - """通过 gateway /whoami 接口验证 Token 并返回用户信息。 - - gateway /whoami 内部完成 RSA 签名验证和过期检查,返回: - - 成功: {"login": true, "user": "...", "is_admin": true/false, ...} - - 失败: {"login": false} - 网络异常(gateway 不可达)按 fail-closed 处理,抛 DnfsExp 拒绝请求。 - """ - if not token: - return None - - req = urllib.request.Request( - _GATEWAY_URL, - data=b'{}', - headers={ - "Content-Type": "application/json", - "Authorization": f"Bearer {token}", - }, - method="POST", - ) - - try: - with urllib.request.urlopen(req, timeout=_GATEWAY_TIMEOUT) as resp: - data = json.loads(resp.read().decode("utf-8")) - except (urllib.error.URLError, urllib.error.HTTPError, json.JSONDecodeError, OSError) as e: - # gateway 不可达或返回异常:fail-closed,拒绝请求 - logs.warning('dnfs', f'Token 验签失败(gateway 异常): {e}', extra={}) - raise DnfsExp('认证服务暂时不可用', 503) - except Exception as e: - logs.warning('dnfs', f'Token 验签未知异常: {e}', extra={}) - raise DnfsExp('认证失败', 401) - - if not data.get('login'): - return None - - # gateway 返回 login=true 时 user 字段必定存在 - return { - 'user': data.get('user', ''), - 'is_admin': bool(data.get('is_admin', False)), - } - - -@app.route('/add_note', methods=['POST']) -@_handle_exp -def add_note(): - # 获取请求数据 - data = request.get_json() - pid: int = data.get('pid') - title: str = data.get('title') - content: str = data.get('content') - if pid is None or not title or not content: - raise DnfsExp("非法参数", 400) - - info = None - auth: str = request.headers.get('Authorization', '') - if auth.startswith('Bearer '): - token: str = auth[7:] - info = decode_token(token) - if info is None: - raise DnfsExp("未授权访问,从"+request.remote_addr, 401) - - - # 父节点是否存在 - if pid != 0: - - select_payload = { - 'action': 'select', - 'table': 'Notes', - 'where': { - 'id': pid - } - } - - notes = _call_dbv2(select_payload) - - if len( notes['data']) == 0: - raise DnfsExp('父节点不存在', 400) - - - # 特殊节点(管理员专属):非管理员拒绝,并做字段/大小校验 - sp = _parse_sp(content) - if sp is not None: - if not info.get('is_admin'): - raise DnfsExp('特殊节点仅管理员可发布,用户'+info.get('user')+',从'+request.remote_addr, 403) - _validate_sp(sp) - - # 内容审查(特殊节点剔除 base64 负载) - review = review_content(_sp_review_text(title, sp) if sp is not None else title + '\n' + content) - if True != review: raise DnfsExp('内容审核不通过,用户'+info.get('user')+',从'+request.remote_addr, 400) - - # 执行插入 - insert_payload = { - 'action': 'insert', - 'table': 'Notes', - 'values': { - "pid": pid, - 'owner': info['user'], - "title": title, - 'content': content - } - } - - result = _call_dbv2(insert_payload) - success = result['success'] - if success is True: - logs.info('dnfs', f'add_note 成功 title={title} pid={pid}', extra={'user': info['user']}) - return jsonify({'success': success}), 200 - else: - raise DnfsExp('插入节点失败,用户'+info.get('user')+',从'+request.remote_addr, 400) - - -# {“pid”: 0} -@_handle_exp -@app.route('/get_notes', methods=["POST"]) -def get_notes(): - - data = request.get_json() - - pid = data.get('pid') - - if not pid: pid = 0 - - select_payload = { - 'action': 'select', - 'table': 'Notes', - 'where': { - 'pid': pid - } - } - - notes = _call_dbv2(select_payload) - - result = notes['data'] - - # 解析当前用户(可选):为每个节点附带操作权标记 can_operate, - # 前端据此显隐编辑/删除按钮。口径与 delete_note / update_note 一致: - # 管理员(is_admin)或节点 owner 有权。 - # get_notes 保持公开:未携带 Token 或 Token 解析失败时,所有节点一律无权。 - info = None - auth: str = request.headers.get('Authorization', '') - if auth.startswith('Bearer '): - try: - info = decode_token(auth[7:]) - except Exception: - info = None - - for note in result: - note['can_operate'] = bool( - info and (info.get('is_admin') or note.get('owner') == info.get('user')) - ) - - logs.info('dnfs', f'get_notes 查询 pid={pid} 返回{len(result)}条', extra={'ip': request.remote_addr}) - return jsonify(result), 200 - - -# eg: {"id": 1} -# [SECURITY-AUDIT] P0-3 / P1-10 —— 已修复 -# 原问题: -# · P0-3 delete_note / update_note 原缺 @_handle_exp 装饰器,异常冒泡成 Flask 默认 500 页。 -# · P1-10 上述函数内 decode_token 等调用无 try-except,异常直接裸抛 500。 -# 修复:两函数均加 @_handle_exp(定义见 L294),DnfsExp → 对应状态码、 -# 其余 Exception → 500,统一返回结构化 JSON,不再泄露栈信息。 -@app.route('/delete_note', methods=['POST']) -@_handle_exp -def delete_note(): - - data = request.get_json() - - note_id: int = data.get('id') - - if not note_id: - - logs.warning('dnfs', 'delete_note 参数缺失 id', extra={'ip': request.remote_addr}) - return jsonify({"error": "Note ID is required"}), 400 - - info = None - auth: str = request.headers.get('Authorization', '') - if auth.startswith('Bearer '): - token: str = auth[7:] - info = decode_token(token) - - if info is None: - logs.warning('dnfs', 'delete_note 未授权访问', extra={'ip': request.remote_addr}) - return jsonify({"error": "Warning: no Authorization"}), 401 - - #with app.app_context(): - - # notes = Note.query.filter(Note.pid == int(note_id)) - - # note = Note.query.get(note_id) - - # if note: - - # db.session.delete(note) - - # db.session.commit() - - # return jsonify({'message': "Note deleted successfully"}), 200 - - # else: - - # return jsonify({"error": "Note not found"}), 404 - - - # 检查是否有子节点 - select_payload = { - 'action': 'select', - 'table': 'Notes', - 'where': { - 'pid': note_id - } - } - - notes = _call_dbv2(select_payload) - - # 有子节点的情况不可删除 - if len(notes['data']) > 0: - - logs.warning('dnfs', f'delete_note 存在子节点 id={note_id}', extra={'user': info['user']}) - return jsonify({'error': 'this node has a child '}), 400 - - # 检查所有权 - select_payload = { - 'action': 'select', - 'table': 'Notes', - 'where': { - 'id': note_id - } - } - - data = _call_dbv2(select_payload) - notes = data['data'] - if not info.get('is_admin'): - if not len(notes) == 1 or not notes[0]['owner'] == info['user']: - logs.warning('dnfs', f'delete_note 所有权拒绝 id={note_id}', extra={'user': info['user']}) - return jsonify({"error": 'It\'s not your note'}), 403 - - - delete_payload = { - 'action': 'delete', - 'table': 'Notes', - 'where': {'id': note_id} - } - - result = _call_dbv2(delete_payload) - - result = result['success'] - - if result is True: - - logs.info('dnfs', f'delete_note 成功 id={note_id}', extra={'user': info['user']}) - return jsonify({'success': True}), 200 - - else: - - logs.error('dnfs', f'delete_note 失败 id={note_id}', extra={'user': info['user']}) - return jsonify({'success': False}), 400 - - -# eg: { -# "id": 1, -# "pid": 0, -# "title": "test", -# "content": "hello" -#} -# [SECURITY-AUDIT] P0-3 / P1-10 —— 同 delete_note,已加 @_handle_exp 统一异常(见上)。 -@app.route('/update_note', methods=['POST']) -@_handle_exp -def update_note(): - - data = request.get_json() - - id: int = data.get('id') - - pid: int = data.get('pid') - - title: str = data.get('title') - - content: str = data.get('content') - - if id is None or pid is None or not title or not content: - - logs.warning('dnfs', 'update_note 参数缺失', extra={'ip': request.remote_addr}) - return jsonify({"error": "Invalid param"}), 400 - - # 获取用户名 - info = None - auth: str = request.headers.get('Authorization', '') - if auth.startswith('Bearer '): - token: str = auth[7:] - info = decode_token(token) - - if info is None: - logs.warning('dnfs', 'update_note 未授权访问', extra={'ip': request.remote_addr}) - return jsonify({"error": "Warning: no Authorization"}), 401 - - - # 检查所有权 - select_payload = { - 'action': 'select', - 'table': 'Notes', - 'where': { - 'id': id - } - } - - data = _call_dbv2(select_payload) - notes = data['data'] - if not info.get('is_admin'): - if not len(notes) == 1 or not notes[0]['owner'] == info['user']: - logs.warning('dnfs', f'update_note 所有权拒绝 id={id}', extra={'user': info['user']}) - return jsonify({"error": 'It\'s not your note'}), 403 - - # 特殊节点(管理员专属):非管理员拒绝,并做字段/大小校验 - sp = _parse_sp(content) - if sp is not None: - if not info.get('is_admin'): - logs.warning('dnfs', f'update_note 特殊节点非管理员拒绝 id={id}', extra={'user': info['user']}) - return jsonify({"error": '特殊节点仅管理员可发布'}), 403 - try: - _validate_sp(sp) - except DnfsExp as e: - return jsonify({"error": str(e)}), e.get_status() - - # 内容审查(特殊节点剔除 base64 负载) - review = review_content(_sp_review_text(title, sp) if sp is not None else title + '\n' + content) - if True != review: - #return jsonify({'error': 'review failed', 'data': review}), 400 - logs.warning('dnfs', f'update_note 内容审核未通过 id={id} title={title}', extra={'user': info['user']}) - return jsonify({'error': 'review failed'}), 400 - - #with app.app_context(): - - # note = Note.query.get(id) - - # if note: - - # note.pid = pid - - # note.title = title - - # note.content = content - - # db.session.commit() - - update_payload = { - "action": "update", - 'table': 'Notes', - "values": { - 'pid': pid, - 'title': title, - 'content': content - }, - 'where': {'id': id} - } - - result = _call_dbv2(update_payload) - - result = result['success'] - - if result is True: - - logs.info('dnfs', f'update_note 成功 id={id} title={title}', extra={'user': info['user']}) - return jsonify({'success': True}), 200 - - else: - - logs.error('dnfs', f'update_note 失败 id={id}', extra={'user': info['user']}) - return jsonify({'success': False}), 400 - -## by shufeng -## 读取单个节点 -'''@app.route("/get_note", methods=["POST"]) - -def get_note(): - data = request.get_json() - - id: int = data.get("id") - auth: str = request.headers.get("Authorization", "") - info = None - - if auth.startswith("Bearer "): - token: str = auth[7:] - info = decode_token(token) - - if id is None: - logs.error("dnfs", "get_note: 参数不足,缺少id") - return jsonify({"error": "Invalid param"}), 401 - - select_payload = { - "method": "select", - "tables": "Notes", - "where": { - "id": id - } - } - - data = _call_dbv2(select_payload) - notes = data["data"] - - if not len(notes) == 1 or not notes[0]["owner"] == info["user"]: - logs.error("dnfs", "没有读取权") - return jsonify({"error": "你没有读取这个的权力"}), 403 - - logs.info("dnfs", f"get_note 查询 id={id}") - return jsonify() -''' - - - -#@app.route('/') -#def view_page(): -# """返回应用主页""" -# return render_template('index.html') - -if __name__ == "__main__": - logs.info('dnfs', f'服务启动,监听 http://{_ADDR}:{_PORT}(db={_DBV2_ADDR}:{_DBV2_PORT}, ai_review={_REVIEW_SERVER_URL})') - print(f'Listen at http://{_ADDR}:{_PORT}') - serve(app, host=_ADDR, port=_PORT) diff --git a/dnfs/log_client.py b/dnfs/log_client.py deleted file mode 100644 index 6a2051e..0000000 --- a/dnfs/log_client.py +++ /dev/null @@ -1,4 +0,0 @@ -"""日志客户端 — 委托到 pymsg.logc 统一库。""" -from pymsg.logc import LogClient, set_log_level # noqa: F401 - -logs = LogClient(service_name="dnfs") diff --git a/dnfs/requirements.txt b/dnfs/requirements.txt deleted file mode 100644 index 3984577..0000000 --- a/dnfs/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -nuitka -flask -flask-cors -flask_sqlalchemy -#gunicorn -waitress -../pymsg diff --git a/dnfs/security_middleware.py b/dnfs/security_middleware.py deleted file mode 100644 index be880f6..0000000 --- a/dnfs/security_middleware.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Security headers middleware for Flask applications. -Usage: - from security_middleware import apply_security_headers - apply_security_headers(app) -""" -import secrets - -def apply_security_headers(app, csp_policy=None): - """Apply security response headers to a Flask app.""" - - @app.after_request - def set_security_headers(response): - # Prevent clickjacking - response.headers['X-Frame-Options'] = 'SAMEORIGIN' - # XSS protection (legacy browsers) - response.headers['X-XSS-Protection'] = '1; mode=block' - # Prevent MIME type sniffing - response.headers['X-Content-Type-Options'] = 'nosniff' - # Referrer policy - response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin' - # Permissions policy (disable camera, mic, geolocation) - response.headers['Permissions-Policy'] = 'camera=(), microphone=(), geolocation=()' - - # Content Security Policy - if csp_policy: - response.headers['Content-Security-Policy'] = csp_policy - else: - response.headers['Content-Security-Policy'] = ( - "default-src 'self'; " - "script-src 'self' 'unsafe-inline'; " - "style-src 'self' 'unsafe-inline'; " - "img-src 'self' data: blob:; " - "font-src 'self'; " - "connect-src 'self'; " - "frame-ancestors 'self'" - ) - - # Remove server header info - response.headers.pop('Server', None) - - return response