78 lines
2.2 KiB
Python
78 lines
2.2 KiB
Python
"""读取并脱敏 .env 配置供 UI 查看(v1.1.18)。
|
||
|
||
仅显示 .env 文件中的非空 key;命中 (TOKEN|SECRET|KEY|PASSWORD) 的字段自动脱敏为 ***。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from pathlib import Path
|
||
|
||
from util.runtime_paths import get_env_path
|
||
|
||
|
||
# 匹配任一含 TOKEN / SECRET / KEY / PASSWORD 的 key 名(不区分大小写)
|
||
_SECRET_PATTERN = re.compile(r"(TOKEN|SECRET|KEY|PASSWORD)", re.IGNORECASE)
|
||
|
||
|
||
def _parse_env_file(path: Path) -> dict[str, str]:
|
||
"""手动解析 .env(避免依赖 python-dotenv)。
|
||
|
||
- 跳过空行 / 注释(# 开头)
|
||
- 支持 KEY=VALUE 格式
|
||
- 自动去除包裹的 "..." 或 '...'
|
||
"""
|
||
if not path.exists():
|
||
return {}
|
||
result: dict[str, str] = {}
|
||
with open(path, "r", encoding="utf-8") as f:
|
||
for line in f:
|
||
line = line.strip()
|
||
if not line or line.startswith("#"):
|
||
continue
|
||
if "=" not in line:
|
||
continue
|
||
k, _, v = line.partition("=")
|
||
k = k.strip()
|
||
v = v.strip()
|
||
# 去掉行尾注释(不处理引号内的 #,简单实现)
|
||
if " #" in v:
|
||
v = v.split(" #", 1)[0].strip()
|
||
if v.startswith('"') and v.endswith('"') and len(v) >= 2:
|
||
v = v[1:-1]
|
||
elif v.startswith("'") and v.endswith("'") and len(v) >= 2:
|
||
v = v[1:-1]
|
||
result[k] = v
|
||
return result
|
||
|
||
|
||
def _mask_secret(value: str) -> str:
|
||
if not value:
|
||
return ""
|
||
if len(value) <= 4:
|
||
return "***"
|
||
return value[:2] + "***" + value[-2:]
|
||
|
||
|
||
def read_env_config() -> dict:
|
||
"""读取 .env 中的非空 key,敏感 key 脱敏为 ***。
|
||
|
||
Returns:
|
||
{
|
||
"env_path": str,
|
||
"exists": bool,
|
||
"config": {key: value_or_masked_value, ...}
|
||
}
|
||
"""
|
||
path = get_env_path()
|
||
parsed = _parse_env_file(path)
|
||
out: dict[str, str] = {}
|
||
for k, v in sorted(parsed.items()):
|
||
if not v:
|
||
continue
|
||
if _SECRET_PATTERN.search(k):
|
||
out[k] = _mask_secret(v)
|
||
else:
|
||
out[k] = v
|
||
return {"env_path": str(path), "exists": path.exists(), "config": out}
|