Compare commits
4 Commits
1ecc0fdc1c
...
19837b4817
| Author | SHA1 | Date | |
|---|---|---|---|
| 19837b4817 | |||
| b5d09009e3 | |||
| 0b521ae8ab | |||
| eac0766e79 |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "ee"]
|
||||||
|
path = ee
|
||||||
|
url = https://git.yxqi.cn/Yuerchu/disknext-ee.git
|
||||||
1
ee
Submodule
1
ee
Submodule
Submodule ee added at 52921f9ffe
@@ -1,42 +0,0 @@
|
|||||||
"""
|
|
||||||
DiskNext Enterprise Edition (EE) 模块
|
|
||||||
|
|
||||||
通过 ``try: from ee import init_ee`` 检测是否存在。
|
|
||||||
CE 版本中此目录不存在,ImportError 被 main.py 捕获。
|
|
||||||
"""
|
|
||||||
from loguru import logger as l
|
|
||||||
|
|
||||||
from utils.conf import appmeta
|
|
||||||
|
|
||||||
_ee_initialized: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
def is_pro() -> bool:
|
|
||||||
"""当前实例是否以 Pro 版本运行。"""
|
|
||||||
return _ee_initialized
|
|
||||||
|
|
||||||
|
|
||||||
async def init_ee() -> None:
|
|
||||||
"""
|
|
||||||
初始化企业版功能。
|
|
||||||
|
|
||||||
1. 加载并验证许可证
|
|
||||||
2. 设置 appmeta.IsPro = True
|
|
||||||
3. 标记 EE 已初始化
|
|
||||||
|
|
||||||
许可证无效或缺失时抛出异常,阻止应用启动。
|
|
||||||
"""
|
|
||||||
global _ee_initialized
|
|
||||||
|
|
||||||
from ee.service.license_service import load_and_validate_license
|
|
||||||
|
|
||||||
payload = await load_and_validate_license()
|
|
||||||
|
|
||||||
appmeta.IsPro = True
|
|
||||||
_ee_initialized = True
|
|
||||||
|
|
||||||
l.info(
|
|
||||||
f"Pro 版本已激活 — 域名: {payload.domain}, "
|
|
||||||
f"过期: {payload.expires_at.isoformat()}, "
|
|
||||||
f"功能: {payload.features}"
|
|
||||||
)
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
"""
|
|
||||||
RSA-PSS 许可证验签核心(编译为 .so 后公钥藏入二进制)
|
|
||||||
|
|
||||||
此文件只包含纯函数和常量,不包含 SQLModel 类。
|
|
||||||
"""
|
|
||||||
import base64
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
import orjson
|
|
||||||
from cryptography.hazmat.primitives import hashes, serialization
|
|
||||||
from cryptography.hazmat.primitives.asymmetric import padding
|
|
||||||
|
|
||||||
|
|
||||||
_PUBLIC_KEY_PEM: bytes = b"""-----BEGIN PUBLIC KEY-----
|
|
||||||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyNltXQ/Nuechx3kjj3T5
|
|
||||||
oR6pZvTmpsDowqqxXJy7FXUI8d7XprhV+HrBQPsrT/Ngo9FwW3XyiK10m1WrzpGW
|
|
||||||
eaf9990Z5Z2naEn5TzGrh71p/D7mZcNGVumo9uAuhtNEemm6xB3FoyGYZj7X0cwA
|
|
||||||
VDvIiKAwYyRJX2LqVh1/tZM6tTO3oaGZXRMZzCNUPFSo4ZZudU3Boa5oQg08evu4
|
|
||||||
vaOqeFrMX47R3MSUmO9hOh+NS53XNqO0f0zw5sv95CtyR5qvJ4gpkgYaRCSQFd19
|
|
||||||
TnHU5saFVrH9jdADz1tdkMYcyYE+uJActZBapxCHSYB2tSCKWjDxeUFl/oY/ZFtY
|
|
||||||
l4MNz1ovkjNhpmR3g+I5fbvN0cxDIjnZ9vJ84ozGqTGT9s1jHaLbpLri/vhuT4F2
|
|
||||||
7kifXk8ImwtMZpZvzhmucH9/5VgcWKNuMATzEMif+YjFpuOGx8gc1XL1W/3q+dH0
|
|
||||||
EFESp+/knjcVIfwpAkIKyV7XvDgFHsif1SeI0zZMW4utowVvGocP1ZzK5BGNTk2z
|
|
||||||
CEtQDO7Rqo+UDckOJSG66VW3c2QO8o6uuy6fzx7q0MFEmUMwGf2iMVtR/KnXe99C
|
|
||||||
enOT0BpU1EQvqssErUqivDss7jm98iD8M/TCE7pFboqZ+SC9G+QAqNIQNFWh8bWA
|
|
||||||
R9hyXM/x5ysHd6MC4eEQnhMCAwEAAQ==
|
|
||||||
-----END PUBLIC KEY-----"""
|
|
||||||
|
|
||||||
|
|
||||||
class LicenseError(Exception):
|
|
||||||
"""许可证验证基础异常"""
|
|
||||||
|
|
||||||
|
|
||||||
class LicenseExpiredError(LicenseError):
|
|
||||||
"""许可证已过期"""
|
|
||||||
|
|
||||||
|
|
||||||
def verify_license(raw: str) -> dict:
|
|
||||||
"""
|
|
||||||
验证许可证字符串并返回载荷字典。
|
|
||||||
|
|
||||||
:param raw: 格式为 ``base64(json_payload).base64(signature)``
|
|
||||||
:returns: 解析后的载荷字典
|
|
||||||
:raises LicenseError: 格式无效或签名验证失败
|
|
||||||
:raises LicenseExpiredError: 许可证已过期
|
|
||||||
"""
|
|
||||||
parts = raw.strip().split(".")
|
|
||||||
if len(parts) != 2:
|
|
||||||
raise LicenseError("许可证格式无效:需要 payload.signature")
|
|
||||||
|
|
||||||
payload_b64, signature_b64 = parts
|
|
||||||
|
|
||||||
try:
|
|
||||||
payload_bytes = base64.urlsafe_b64decode(payload_b64)
|
|
||||||
signature = base64.urlsafe_b64decode(signature_b64)
|
|
||||||
except Exception as exc:
|
|
||||||
raise LicenseError(f"许可证 base64 解码失败: {exc}") from exc
|
|
||||||
|
|
||||||
public_key = serialization.load_pem_public_key(_PUBLIC_KEY_PEM)
|
|
||||||
|
|
||||||
try:
|
|
||||||
public_key.verify( # type: ignore[union-attr]
|
|
||||||
signature,
|
|
||||||
payload_bytes,
|
|
||||||
padding.PSS(
|
|
||||||
mgf=padding.MGF1(hashes.SHA256()),
|
|
||||||
salt_length=padding.PSS.MAX_LENGTH,
|
|
||||||
),
|
|
||||||
hashes.SHA256(),
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
raise LicenseError(f"许可证签名验证失败: {exc}") from exc
|
|
||||||
|
|
||||||
data: dict = orjson.loads(payload_bytes)
|
|
||||||
|
|
||||||
expires_at_str: str | None = data.get('expires_at')
|
|
||||||
if not expires_at_str:
|
|
||||||
raise LicenseError("许可证缺少 expires_at 字段")
|
|
||||||
|
|
||||||
expires_at = datetime.fromisoformat(expires_at_str)
|
|
||||||
if expires_at < datetime.now(timezone.utc):
|
|
||||||
raise LicenseExpiredError(
|
|
||||||
f"许可证已过期: {expires_at.isoformat()}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return data
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
from fastapi import APIRouter
|
|
||||||
|
|
||||||
from .pro import router as pro_router
|
|
||||||
|
|
||||||
ee_router = APIRouter()
|
|
||||||
ee_router.include_router(pro_router)
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
"""
|
|
||||||
Pro 版本状态端点
|
|
||||||
|
|
||||||
提供许可证状态查询,需管理员权限。
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
|
||||||
|
|
||||||
from ee.service import LicensePayload
|
|
||||||
from ee.service.license_service import get_cached_license
|
|
||||||
from middleware.auth import admin_required
|
|
||||||
from sqlmodel_ext import SQLModelBase
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/pro")
|
|
||||||
|
|
||||||
|
|
||||||
class ProStatusResponse(SQLModelBase):
|
|
||||||
"""Pro 版本状态响应"""
|
|
||||||
|
|
||||||
is_active: bool
|
|
||||||
"""许可证是否有效"""
|
|
||||||
|
|
||||||
domain: str
|
|
||||||
"""授权域名"""
|
|
||||||
|
|
||||||
expires_at: datetime
|
|
||||||
"""过期时间"""
|
|
||||||
|
|
||||||
max_users: int
|
|
||||||
"""最大用户数(0 = 无限制)"""
|
|
||||||
|
|
||||||
features: list[str]
|
|
||||||
"""已授权的功能列表"""
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
'/status',
|
|
||||||
dependencies=[Depends(admin_required)],
|
|
||||||
)
|
|
||||||
async def get_pro_status() -> ProStatusResponse:
|
|
||||||
"""
|
|
||||||
查询 Pro 版本许可证状态
|
|
||||||
|
|
||||||
认证:
|
|
||||||
- JWT token in Authorization header
|
|
||||||
- 需要管理员权限
|
|
||||||
|
|
||||||
响应:
|
|
||||||
- ProStatusResponse: 当前许可证信息
|
|
||||||
|
|
||||||
错误处理:
|
|
||||||
- HTTPException 401: 未授权
|
|
||||||
- HTTPException 403: 非管理员
|
|
||||||
- HTTPException 500: 许可证缓存异常
|
|
||||||
"""
|
|
||||||
payload: LicensePayload | None = get_cached_license()
|
|
||||||
if not payload:
|
|
||||||
# init_ee 成功后 payload 一定存在,此处做防御性编程
|
|
||||||
from utils.http.http_exceptions import raise_internal_error
|
|
||||||
raise_internal_error()
|
|
||||||
|
|
||||||
return ProStatusResponse(
|
|
||||||
is_active=True,
|
|
||||||
domain=payload.domain,
|
|
||||||
expires_at=payload.expires_at,
|
|
||||||
max_users=payload.max_users,
|
|
||||||
features=payload.features,
|
|
||||||
)
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
"""
|
|
||||||
EE 许可证服务模块
|
|
||||||
|
|
||||||
提供许可证加载、验证和缓存功能。
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from sqlmodel_ext import SQLModelBase
|
|
||||||
|
|
||||||
|
|
||||||
class LicensePayload(SQLModelBase):
|
|
||||||
"""许可证载荷(RSA 验签后的明文数据)"""
|
|
||||||
|
|
||||||
domain: str
|
|
||||||
"""授权域名"""
|
|
||||||
|
|
||||||
expires_at: datetime
|
|
||||||
"""过期时间(UTC)"""
|
|
||||||
|
|
||||||
max_users: int
|
|
||||||
"""最大用户数(0 = 无限制)"""
|
|
||||||
|
|
||||||
features: list[str]
|
|
||||||
"""已授权的功能列表"""
|
|
||||||
|
|
||||||
issued_at: datetime
|
|
||||||
"""签发时间(UTC)"""
|
|
||||||
|
|
||||||
|
|
||||||
from .license_service import get_cached_license, load_and_validate_license
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
"""
|
|
||||||
许可证加载与缓存服务(编译为 .so)
|
|
||||||
|
|
||||||
从环境变量 LICENSE_KEY 或 license.key 文件加载许可证,
|
|
||||||
调用 verify_license() 验证后缓存结果。
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import aiofiles
|
|
||||||
|
|
||||||
from ee.license import LicenseError, verify_license
|
|
||||||
from ee.service import LicensePayload
|
|
||||||
|
|
||||||
_cached_payload: LicensePayload | None = None
|
|
||||||
|
|
||||||
|
|
||||||
async def load_and_validate_license() -> LicensePayload:
|
|
||||||
"""
|
|
||||||
加载并验证许可证,成功后缓存。
|
|
||||||
|
|
||||||
加载优先级:
|
|
||||||
1. 环境变量 ``LICENSE_KEY``
|
|
||||||
2. 项目根目录 ``license.key`` 文件
|
|
||||||
|
|
||||||
:returns: 验证通过的 LicensePayload
|
|
||||||
:raises LicenseError: 未找到许可证 / 验证失败 / 已过期
|
|
||||||
"""
|
|
||||||
global _cached_payload
|
|
||||||
|
|
||||||
raw: str | None = os.getenv("LICENSE_KEY")
|
|
||||||
|
|
||||||
if not raw:
|
|
||||||
key_path = Path("license.key")
|
|
||||||
if key_path.is_file():
|
|
||||||
async with aiofiles.open(key_path, 'r') as f:
|
|
||||||
raw = (await f.read()).strip()
|
|
||||||
|
|
||||||
if not raw:
|
|
||||||
raise LicenseError("未找到许可证:请设置 LICENSE_KEY 环境变量或提供 license.key 文件")
|
|
||||||
|
|
||||||
data = verify_license(raw)
|
|
||||||
_cached_payload = LicensePayload.model_validate(data)
|
|
||||||
return _cached_payload
|
|
||||||
|
|
||||||
|
|
||||||
def get_cached_license() -> LicensePayload | None:
|
|
||||||
"""获取已缓存的许可证载荷(未加载时返回 None)。"""
|
|
||||||
return _cached_payload
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
"""
|
|
||||||
EE 版本数据库模型
|
|
||||||
|
|
||||||
后续 Pro 功能的 SQLModel 定义位置。
|
|
||||||
"""
|
|
||||||
Reference in New Issue
Block a user