chore: 删除已废弃的 dnfs 模块,由 dnfs2 (C++20) 平替
This commit is contained in:
1 parent
1190a15e1e
commit
0a79a6f631
12 files changed
-1351
No files matched your search
@@ -35,7 +35,6 @@ inst_module "gateway"
|
|||||||
inst_module "waf" # waf 在 gateway 之前编译,部署时由 core 先拉起 waf 再拉起 gateway
|
inst_module "waf" # waf 在 gateway 之前编译,部署时由 core 先拉起 waf 再拉起 gateway
|
||||||
inst_module "ca"
|
inst_module "ca"
|
||||||
inst_module "fsys"
|
inst_module "fsys"
|
||||||
inst_module "dnfs"
|
|
||||||
inst_module "dnfs2"
|
inst_module "dnfs2"
|
||||||
inst_module "db"
|
inst_module "db"
|
||||||
inst_module "db2"
|
inst_module "db2"
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
.env
|
|
||||||
.env/
|
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
*.pyo
|
|
||||||
*.pyd
|
|
||||||
.Python
|
|
||||||
*.so
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
*.md
|
|
||||||
!README.md
|
|
||||||
.envrc
|
|
||||||
.venv/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
*.bin
|
|
||||||
.DS_Store
|
|
||||||
-112
@@ -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/
|
|
||||||
@@ -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"]
|
|
||||||
@@ -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.
|
|
||||||
-347
@@ -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 <base64_payload>_<rsa_sign>
|
|
||||||
```
|
|
||||||
|
|
||||||
- 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 <token>`。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8006/add_note \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-H 'Authorization: Bearer <token>' \
|
|
||||||
-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 <token>`。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8006/delete_note \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-H 'Authorization: Bearer <token>' \
|
|
||||||
-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 <token>`。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8006/update_note \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-H 'Authorization: Bearer <token>' \
|
|
||||||
-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 代码和已废弃接口,影响可读性
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
cd "$(dirname "$0")" || exit
|
|
||||||
source ./.env/bin/activate
|
|
||||||
#exec ./dnfs.bin
|
|
||||||
exec python3 dnfs.py
|
|
||||||
@@ -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
|
|
||||||
-739
@@ -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)
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
"""日志客户端 — 委托到 pymsg.logc 统一库。"""
|
|
||||||
from pymsg.logc import LogClient, set_log_level # noqa: F401
|
|
||||||
|
|
||||||
logs = LogClient(service_name="dnfs")
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
nuitka
|
|
||||||
flask
|
|
||||||
flask-cors
|
|
||||||
flask_sqlalchemy
|
|
||||||
#gunicorn
|
|
||||||
waitress
|
|
||||||
../pymsg
|
|
||||||
@@ -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
|
|
||||||
Reference in new issue
Block a user