Commit ·
63bfd99
1
Parent(s): c49ad87
refactor(proxy): 代理 URL 不再做密码遮蔽
Browse files- proxy_service 删除 _mask_proxy_url,get_public 返回原始 URL
- /api/proxy 与 /api/proxy/test 去掉 ***@ 特殊处理
- 设置页去掉显示/隐藏切换,直接展示完整 URL
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- services/api.py +2 -10
- services/proxy_service.py +2 -20
- web/src/app/settings/components/proxy-settings.tsx +6 -20
services/api.py
CHANGED
|
@@ -580,15 +580,8 @@ def create_app() -> FastAPI:
|
|
| 580 |
@router.post("/api/proxy")
|
| 581 |
async def update_proxy(body: ProxyUpdateRequest, authorization: str | None = Header(default=None)):
|
| 582 |
require_auth_key(authorization)
|
| 583 |
-
|
| 584 |
-
# If the client echoes back the masked form (contains `***@`), treat it as "no change"
|
| 585 |
-
# so the stored password is preserved.
|
| 586 |
-
url = body.url
|
| 587 |
-
if url is not None and "***@" in url:
|
| 588 |
-
url = None
|
| 589 |
-
|
| 590 |
try:
|
| 591 |
-
proxy_config.update(enabled=body.enabled, url=url)
|
| 592 |
except ValueError as exc:
|
| 593 |
raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc
|
| 594 |
return {"proxy": proxy_config.get_public()}
|
|
@@ -600,8 +593,7 @@ def create_app() -> FastAPI:
|
|
| 600 |
):
|
| 601 |
require_auth_key(authorization)
|
| 602 |
candidate = (body.url or "").strip()
|
| 603 |
-
if not candidate
|
| 604 |
-
# Fall back to the stored url when caller omits it or sent the masked form.
|
| 605 |
candidate = (proxy_config.get().get("url") or "").strip()
|
| 606 |
if not candidate:
|
| 607 |
raise HTTPException(status_code=400, detail={"error": "proxy url is required"})
|
|
|
|
| 580 |
@router.post("/api/proxy")
|
| 581 |
async def update_proxy(body: ProxyUpdateRequest, authorization: str | None = Header(default=None)):
|
| 582 |
require_auth_key(authorization)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 583 |
try:
|
| 584 |
+
proxy_config.update(enabled=body.enabled, url=body.url)
|
| 585 |
except ValueError as exc:
|
| 586 |
raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc
|
| 587 |
return {"proxy": proxy_config.get_public()}
|
|
|
|
| 593 |
):
|
| 594 |
require_auth_key(authorization)
|
| 595 |
candidate = (body.url or "").strip()
|
| 596 |
+
if not candidate:
|
|
|
|
| 597 |
candidate = (proxy_config.get().get("url") or "").strip()
|
| 598 |
if not candidate:
|
| 599 |
raise HTTPException(status_code=400, detail={"error": "proxy url is required"})
|
services/proxy_service.py
CHANGED
|
@@ -11,7 +11,7 @@ import os
|
|
| 11 |
import time
|
| 12 |
from pathlib import Path
|
| 13 |
from threading import Lock
|
| 14 |
-
from urllib.parse import
|
| 15 |
|
| 16 |
from curl_cffi.requests import Session
|
| 17 |
|
|
@@ -37,23 +37,6 @@ def _is_valid_proxy_url(url: str) -> bool:
|
|
| 37 |
return bool(parsed.hostname)
|
| 38 |
|
| 39 |
|
| 40 |
-
def _mask_proxy_url(url: str) -> str:
|
| 41 |
-
"""Return a display-safe version of the proxy URL where password is hidden."""
|
| 42 |
-
if not url:
|
| 43 |
-
return ""
|
| 44 |
-
try:
|
| 45 |
-
parsed = urlparse(url)
|
| 46 |
-
except Exception:
|
| 47 |
-
return url
|
| 48 |
-
if not parsed.password:
|
| 49 |
-
return url
|
| 50 |
-
user = parsed.username or ""
|
| 51 |
-
host = parsed.hostname or ""
|
| 52 |
-
port = f":{parsed.port}" if parsed.port else ""
|
| 53 |
-
netloc = f"{quote(user, safe='')}:***@{host}{port}"
|
| 54 |
-
return urlunparse(parsed._replace(netloc=netloc))
|
| 55 |
-
|
| 56 |
-
|
| 57 |
class ProxyConfig:
|
| 58 |
def __init__(self, store_file: Path):
|
| 59 |
self._store_file = store_file
|
|
@@ -92,11 +75,10 @@ class ProxyConfig:
|
|
| 92 |
return dict(self._state)
|
| 93 |
|
| 94 |
def get_public(self) -> dict:
|
| 95 |
-
"""Return a masked representation safe to expose over the API."""
|
| 96 |
state = self.get()
|
| 97 |
return {
|
| 98 |
"enabled": bool(state.get("enabled")),
|
| 99 |
-
"url":
|
| 100 |
}
|
| 101 |
|
| 102 |
def update(self, *, enabled: bool | None, url: str | None) -> dict:
|
|
|
|
| 11 |
import time
|
| 12 |
from pathlib import Path
|
| 13 |
from threading import Lock
|
| 14 |
+
from urllib.parse import urlparse
|
| 15 |
|
| 16 |
from curl_cffi.requests import Session
|
| 17 |
|
|
|
|
| 37 |
return bool(parsed.hostname)
|
| 38 |
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
class ProxyConfig:
|
| 41 |
def __init__(self, store_file: Path):
|
| 42 |
self._store_file = store_file
|
|
|
|
| 75 |
return dict(self._state)
|
| 76 |
|
| 77 |
def get_public(self) -> dict:
|
|
|
|
| 78 |
state = self.get()
|
| 79 |
return {
|
| 80 |
"enabled": bool(state.get("enabled")),
|
| 81 |
+
"url": _clean(state.get("url")),
|
| 82 |
}
|
| 83 |
|
| 84 |
def update(self, *, enabled: bool | None, url: str | None) -> dict:
|
web/src/app/settings/components/proxy-settings.tsx
CHANGED
|
@@ -2,8 +2,6 @@
|
|
| 2 |
|
| 3 |
import { useEffect, useRef, useState } from "react";
|
| 4 |
import {
|
| 5 |
-
Eye,
|
| 6 |
-
EyeOff,
|
| 7 |
LoaderCircle,
|
| 8 |
PlugZap,
|
| 9 |
Save,
|
|
@@ -27,7 +25,6 @@ export function ProxySettingsCard() {
|
|
| 27 |
const [settings, setSettings] = useState<ProxySettings>({ enabled: false, url: "" });
|
| 28 |
const [formUrl, setFormUrl] = useState("");
|
| 29 |
const [formEnabled, setFormEnabled] = useState(false);
|
| 30 |
-
const [showUrl, setShowUrl] = useState(false);
|
| 31 |
const [isLoading, setIsLoading] = useState(true);
|
| 32 |
const [isSaving, setIsSaving] = useState(false);
|
| 33 |
const [isTesting, setIsTesting] = useState(false);
|
|
@@ -147,25 +144,14 @@ export function ProxySettingsCard() {
|
|
| 147 |
<PlugZap className="size-3.5" />
|
| 148 |
代理地址
|
| 149 |
</label>
|
| 150 |
-
<
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
className="h-11 rounded-xl border-stone-200 bg-white pr-10 font-mono text-xs"
|
| 157 |
-
/>
|
| 158 |
-
<button
|
| 159 |
-
type="button"
|
| 160 |
-
className="absolute top-1/2 right-3 -translate-y-1/2 text-stone-400 transition hover:text-stone-600"
|
| 161 |
-
onClick={() => setShowUrl((prev) => !prev)}
|
| 162 |
-
>
|
| 163 |
-
{showUrl ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
| 164 |
-
</button>
|
| 165 |
-
</div>
|
| 166 |
<div className="text-xs text-stone-400">
|
| 167 |
支持 <code className="font-mono">http / https / socks4 / socks5 / socks5h</code>。
|
| 168 |
-
密码部分展示时会被遮蔽(<code className="font-mono">***</code>),回传时保持原样即可保留密码。
|
| 169 |
</div>
|
| 170 |
</div>
|
| 171 |
|
|
|
|
| 2 |
|
| 3 |
import { useEffect, useRef, useState } from "react";
|
| 4 |
import {
|
|
|
|
|
|
|
| 5 |
LoaderCircle,
|
| 6 |
PlugZap,
|
| 7 |
Save,
|
|
|
|
| 25 |
const [settings, setSettings] = useState<ProxySettings>({ enabled: false, url: "" });
|
| 26 |
const [formUrl, setFormUrl] = useState("");
|
| 27 |
const [formEnabled, setFormEnabled] = useState(false);
|
|
|
|
| 28 |
const [isLoading, setIsLoading] = useState(true);
|
| 29 |
const [isSaving, setIsSaving] = useState(false);
|
| 30 |
const [isTesting, setIsTesting] = useState(false);
|
|
|
|
| 144 |
<PlugZap className="size-3.5" />
|
| 145 |
代理地址
|
| 146 |
</label>
|
| 147 |
+
<Input
|
| 148 |
+
value={formUrl}
|
| 149 |
+
onChange={(event) => setFormUrl(event.target.value)}
|
| 150 |
+
placeholder="http://user:pass@host:port 或 socks5://host:port"
|
| 151 |
+
className="h-11 rounded-xl border-stone-200 bg-white font-mono text-xs"
|
| 152 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
<div className="text-xs text-stone-400">
|
| 154 |
支持 <code className="font-mono">http / https / socks4 / socks5 / socks5h</code>。
|
|
|
|
| 155 |
</div>
|
| 156 |
</div>
|
| 157 |
|