diff --git a/routes/webdav.py b/routes/webdav.py index 60a2705..4710ac7 100644 --- a/routes/webdav.py +++ b/routes/webdav.py @@ -4,20 +4,43 @@ WebDAV 文件管理路由 """ import os +import secrets import shutil import mimetypes from pathlib import Path from datetime import datetime, timezone from xml.etree import ElementTree as ET -from fastapi import APIRouter, Request, HTTPException, Response +from fastapi import APIRouter, Request, HTTPException, Response, Depends from fastapi.responses import FileResponse, Response as FastAPIResponse +from fastapi.security import HTTPBasic, HTTPBasicCredentials import logging +from utils.settings import WEBDAV_USERNAME, WEBDAV_PASSWORD + logger = logging.getLogger('app') +security = HTTPBasic(realm="WebDAV") + + +def verify_credentials(credentials: HTTPBasicCredentials = Depends(security)): + """验证 WebDAV Basic Auth 账号密码""" + username_correct = secrets.compare_digest(credentials.username, WEBDAV_USERNAME) + password_correct = secrets.compare_digest(credentials.password, WEBDAV_PASSWORD) + if not (username_correct and password_correct): + raise HTTPException( + status_code=401, + detail="认证失败", + headers={"WWW-Authenticate": 'Basic realm="WebDAV"'}, + ) + return credentials + +# 不在 router 级别加认证,OPTIONS 需要免认证(macOS Finder 等客户端预检不带凭据) router = APIRouter(prefix="/webdav", tags=["webdav"]) +# 需要认证的依赖,用于非 OPTIONS 的端点 +auth_dep = [Depends(verify_credentials)] + # WebDAV XML 命名空间 DAV_NS = "DAV:" DAV_PREFIX = "{DAV:}" @@ -107,8 +130,8 @@ def build_multistatus(responses: list[ET.Element]) -> str: # ===== PROPFIND ===== -@router.api_route("/{bot_id}/{path:path}", methods=["PROPFIND"]) -@router.api_route("/{bot_id}", methods=["PROPFIND"]) +@router.api_route("/{bot_id}/{path:path}", methods=["PROPFIND"], dependencies=auth_dep) +@router.api_route("/{bot_id}", methods=["PROPFIND"], dependencies=auth_dep) async def propfind(bot_id: str, request: Request, path: str = ""): """PROPFIND - 获取资源属性(列出目录)""" bot_root = get_bot_root(bot_id) @@ -166,7 +189,7 @@ async def options(bot_id: str, path: str = ""): # ===== GET ===== -@router.get("/{bot_id}/{path:path}") +@router.get("/{bot_id}/{path:path}", dependencies=auth_dep) async def get_file(bot_id: str, path: str): """GET - 下载文件""" bot_root = get_bot_root(bot_id) @@ -192,7 +215,7 @@ async def get_file(bot_id: str, path: str): # ===== HEAD ===== -@router.head("/{bot_id}/{path:path}") +@router.head("/{bot_id}/{path:path}", dependencies=auth_dep) async def head_file(bot_id: str, path: str): """HEAD - 获取文件元数据""" bot_root = get_bot_root(bot_id) @@ -214,7 +237,7 @@ async def head_file(bot_id: str, path: str): # ===== PUT ===== -@router.put("/{bot_id}/{path:path}") +@router.put("/{bot_id}/{path:path}", dependencies=auth_dep) async def put_file(bot_id: str, path: str, request: Request): """PUT - 上传/覆盖文件""" bot_root = get_bot_root(bot_id) @@ -234,7 +257,7 @@ async def put_file(bot_id: str, path: str, request: Request): # ===== DELETE ===== -@router.delete("/{bot_id}/{path:path}") +@router.delete("/{bot_id}/{path:path}", dependencies=auth_dep) async def delete_resource(bot_id: str, path: str): """DELETE - 删除文件或目录""" if not path.strip("/"): @@ -256,7 +279,7 @@ async def delete_resource(bot_id: str, path: str): # ===== MKCOL ===== -@router.api_route("/{bot_id}/{path:path}", methods=["MKCOL"]) +@router.api_route("/{bot_id}/{path:path}", methods=["MKCOL"], dependencies=auth_dep) async def mkcol(bot_id: str, path: str): """MKCOL - 创建目录""" bot_root = get_bot_root(bot_id) @@ -274,7 +297,7 @@ async def mkcol(bot_id: str, path: str): # ===== COPY ===== -@router.api_route("/{bot_id}/{path:path}", methods=["COPY"]) +@router.api_route("/{bot_id}/{path:path}", methods=["COPY"], dependencies=auth_dep) async def copy_resource(bot_id: str, path: str, request: Request): """COPY - 复制文件或目录""" bot_root = get_bot_root(bot_id) @@ -315,7 +338,7 @@ async def copy_resource(bot_id: str, path: str, request: Request): # ===== MOVE ===== -@router.api_route("/{bot_id}/{path:path}", methods=["MOVE"]) +@router.api_route("/{bot_id}/{path:path}", methods=["MOVE"], dependencies=auth_dep) async def move_resource(bot_id: str, path: str, request: Request): """MOVE - 移动/重命名文件或目录""" bot_root = get_bot_root(bot_id) diff --git a/utils/settings.py b/utils/settings.py index 76afa3f..61c38bc 100644 --- a/utils/settings.py +++ b/utils/settings.py @@ -39,6 +39,10 @@ TOOL_OUTPUT_TRUNCATION_STRATEGY = os.getenv("TOOL_OUTPUT_TRUNCATION_STRATEGY", " DEFAULT_THINKING_ENABLE = os.getenv("DEFAULT_THINKING_ENABLE", "true") == "true" +# WebDAV Authentication +WEBDAV_USERNAME = os.getenv("WEBDAV_USERNAME", "admin") +WEBDAV_PASSWORD = os.getenv("WEBDAV_PASSWORD", "MmL85TjjxZC97hk9rsYfhQ") + # MCP Tool Timeout Settings MCP_HTTP_TIMEOUT = int(os.getenv("MCP_HTTP_TIMEOUT", 60)) # HTTP 请求超时(秒) MCP_SSE_READ_TIMEOUT = int(os.getenv("MCP_SSE_READ_TIMEOUT", 300)) # SSE 读取超时(秒)