diff --git a/src/core/upload/sub2api_upload.py b/src/core/upload/sub2api_upload.py index 11d0f497..d945b92c 100644 --- a/src/core/upload/sub2api_upload.py +++ b/src/core/upload/sub2api_upload.py @@ -7,6 +7,7 @@ import logging from datetime import datetime, timezone from typing import List, Tuple, Optional +from urllib.parse import urlsplit from curl_cffi import requests as cffi_requests @@ -16,6 +17,26 @@ logger = logging.getLogger(__name__) +def _normalize_sub2api_base_url(api_url: str) -> str: + """清洗并校验 Sub2API 基础地址,避免隐藏字符导致 curl 解析失败。""" + normalized = (api_url or "").strip().strip("'\"") + if not normalized: + raise ValueError("Sub2API URL 未配置") + + parsed = urlsplit(normalized) + if parsed.scheme not in ("http", "https"): + raise ValueError("Sub2API URL 必须以 http:// 或 https:// 开头") + if not parsed.netloc: + raise ValueError("Sub2API URL 缺少主机名") + + try: + _ = parsed.port + except ValueError as exc: + raise ValueError(f"Sub2API URL 端口无效: {exc}") from exc + + return normalized.rstrip("/") + + def upload_to_sub2api( accounts: List[Account], api_url: str, @@ -46,6 +67,11 @@ def upload_to_sub2api( if not api_key: return False, "Sub2API API Key 未配置" + try: + api_url = _normalize_sub2api_base_url(api_url) + except ValueError as e: + return False, str(e) + exported_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") account_items = [] @@ -99,7 +125,7 @@ def upload_to_sub2api( "skip_default_group_bind": True, } - url = api_url.rstrip("/") + "/api/v1/admin/accounts/data" + url = api_url + "/api/v1/admin/accounts/data" headers = { "Content-Type": "application/json", "x-api-key": api_key, @@ -197,7 +223,12 @@ def test_sub2api_connection(api_url: str, api_key: str) -> Tuple[bool, str]: if not api_key: return False, "API Key 不能为空" - url = api_url.rstrip("/") + "/api/v1/admin/accounts/data" + try: + api_url = _normalize_sub2api_base_url(api_url) + except ValueError as e: + return False, str(e) + + url = api_url + "/api/v1/admin/accounts/data" headers = {"x-api-key": api_key} try: diff --git a/src/web/routes/upload/sub2api_services.py b/src/web/routes/upload/sub2api_services.py index 653f4b19..8ee5f313 100644 --- a/src/web/routes/upload/sub2api_services.py +++ b/src/web/routes/upload/sub2api_services.py @@ -8,7 +8,11 @@ from ....database import crud from ....database.session import get_db -from ....core.upload.sub2api_upload import test_sub2api_connection, batch_upload_to_sub2api +from ....core.upload.sub2api_upload import ( + test_sub2api_connection, + batch_upload_to_sub2api, + _normalize_sub2api_base_url, +) router = APIRouter() @@ -85,11 +89,15 @@ async def list_sub2api_services(enabled: Optional[bool] = None): @router.post("", response_model=Sub2ApiServiceResponse) async def create_sub2api_service(request: Sub2ApiServiceCreate): """新增 Sub2API 服务""" + try: + api_url = _normalize_sub2api_base_url(request.api_url) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) from e with get_db() as db: svc = crud.create_sub2api_service( db, - name=request.name, - api_url=request.api_url, + name=request.name.strip(), + api_url=api_url, api_key=request.api_key, target_type=request.target_type, enabled=request.enabled, @@ -136,9 +144,12 @@ async def update_sub2api_service(service_id: int, request: Sub2ApiServiceUpdate) update_data = {} if request.name is not None: - update_data["name"] = request.name + update_data["name"] = request.name.strip() if request.api_url is not None: - update_data["api_url"] = request.api_url + try: + update_data["api_url"] = _normalize_sub2api_base_url(request.api_url) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) from e # api_key 留空则保持原值 if request.api_key: update_data["api_key"] = request.api_key diff --git a/static/js/settings.js b/static/js/settings.js index 46aba297..78a1e661 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -1513,9 +1513,9 @@ async function handleSaveSub2ApiService(e) { e.preventDefault(); const id = document.getElementById('sub2api-service-id').value; const data = { - name: document.getElementById('sub2api-service-name').value, - api_url: document.getElementById('sub2api-service-url').value, - api_key: document.getElementById('sub2api-service-key').value || undefined, + name: document.getElementById('sub2api-service-name').value.trim(), + api_url: document.getElementById('sub2api-service-url').value.trim(), + api_key: document.getElementById('sub2api-service-key').value.trim() || undefined, priority: parseInt(document.getElementById('sub2api-service-priority').value) || 0, enabled: document.getElementById('sub2api-service-enabled').checked, };