revert(login): remove anonymous wallpaper proxy (#6201)

This commit is contained in:
InfinityPacer
2026-07-27 13:48:52 +08:00
committed by GitHub
parent bb00814d7a
commit 8788dae34b
4 changed files with 7 additions and 733 deletions

View File

@@ -1,8 +1,7 @@
from datetime import timedelta
from typing import Annotated, Any, List
from urllib.parse import quote, urlparse, urlunparse
from typing import Any, List, Annotated
from fastapi import APIRouter, Depends, Form, Header, HTTPException, Request, Response
from fastapi import APIRouter, Depends, Form, HTTPException, Request, Response
from fastapi.security import OAuth2PasswordRequestForm
from fastapi.responses import JSONResponse
@@ -12,19 +11,11 @@ from app.core import security
from app.core.config import settings
from app.db.systemconfig_oper import SystemConfigOper
from app.helper.sites import SitesHelper # noqa
from app.helper.image import ImageHelper, WallpaperHelper
from app.helper.image import WallpaperHelper
from app.schemas.types import SystemConfigKey
from app.utils.crypto import HashUtils
from app.utils.http import RequestUtils
from app.utils.security import SecurityUtils
from app.utils.url import UrlUtils
router = APIRouter()
_LOGIN_WALLPAPER_MAX_BYTES = 32 * 1024 * 1024
_LOGIN_WALLPAPER_PUBLIC_PURPOSE = "login-wallpaper-public"
_LOGIN_WALLPAPER_MEDIA_PURPOSE = "login-wallpaper-media"
@router.post("/access-token", summary="获取token", response_model=schemas.Token)
def login_access_token(
@@ -104,134 +95,8 @@ def wallpaper() -> Any:
@router.get("/wallpapers", summary="登录页面电影海报列表", response_model=List[str])
def wallpapers(same_origin: bool = False) -> Any:
def wallpapers() -> Any:
"""
获取登录页面电影海报
默认保持外链列表合同;同源模式只对绝对 HTTP(S) 地址做一对一签名转换,不改变
来源数量、顺序、重复项或相对地址。
获取登录页面电影海报
"""
wallpaper_urls = WallpaperHelper().get_wallpapers()
if not same_origin:
return wallpaper_urls
purpose = (
_LOGIN_WALLPAPER_MEDIA_PURPOSE
if settings.WALLPAPER == "mediaserver"
else _LOGIN_WALLPAPER_PUBLIC_PURPOSE
)
return [_login_wallpaper_proxy_url(url, purpose) for url in wallpaper_urls]
def _login_wallpaper_proxy_url(url: str, purpose: str) -> str:
"""
将可代理的绝对壁纸地址转换为登录页专用同源签名地址。
`//cdn.example/one.jpg` 这类网络路径引用会被浏览器解析成跨源地址,因此按
当前页面协议补全后同样走代理;只有不带 netloc 的相对地址才原样返回。
"""
parsed = urlparse(url)
if not parsed.netloc:
return url
if not parsed.scheme:
url = urlunparse(parsed._replace(scheme="https"))
elif parsed.scheme not in {"http", "https"}:
return url
signed_url = SecurityUtils.sign_url(url, purpose=purpose)
return (
f"{settings.API_V1_STR}/login/wallpapers/image"
f"?url={quote(signed_url, safe='')}"
)
def _url_origin(url: str) -> tuple[str, str, int | None] | None:
"""返回 HTTP(S) 地址的规范 origin拒绝缺少主机或携带用户信息的地址。"""
try:
parsed = urlparse(url)
port = parsed.port
except ValueError:
return None
if (
parsed.scheme not in {"http", "https"}
or not parsed.hostname
or parsed.username
or parsed.password
):
return None
if port is None:
port = 443 if parsed.scheme == "https" else 80
return parsed.scheme, parsed.hostname.lower(), port
@router.get("/wallpapers/image", summary="登录页面同源壁纸")
async def wallpaper_image(
url: str,
if_none_match: Annotated[str | None, Header()] = None,
) -> Response:
"""
读取后端壁纸来源签发的图片;客户端无法修改目标后继续复用签名。
响应带内容 ETag缓存过期后条件请求命中时只返回 304。
"""
source_url = SecurityUtils.verify_signed_url(
url, purpose=_LOGIN_WALLPAPER_PUBLIC_PURPOSE
)
media_source = False
if not source_url:
source_url = SecurityUtils.verify_signed_url(
url, purpose=_LOGIN_WALLPAPER_MEDIA_PURPOSE
)
media_source = bool(source_url)
source_origin = _url_origin(source_url or "")
if not source_url or not source_origin:
raise HTTPException(status_code=404, detail="Wallpaper not found")
async def is_safe_public_target(target_url: str) -> bool:
"""自定义公共来源可跨域跳转,但每个目标都必须通过 DNS/私网校验。"""
target_origin = _url_origin(target_url)
if not target_origin:
return False
return await SecurityUtils.is_safe_image_url_async(
target_url,
{target_origin[1]},
allowed_private_ranges=settings.IMAGE_PROXY_ALLOWED_PRIVATE_RANGES,
)
async def is_safe_redirect(target_url: str) -> bool:
"""媒体服务器签名只授权原 origin其它跳转按公共图片目标重新校验。"""
target_origin = _url_origin(target_url)
if media_source and target_origin == source_origin:
return True
return await is_safe_public_target(target_url)
if not media_source and not await is_safe_public_target(source_url):
raise HTTPException(status_code=404, detail="Wallpaper not found")
# 媒体服务器来源额外继承原 origin 的私网授权,与公共来源的重定向授权范围不同,
# 因此按来源类型声明策略标识,只让同策略的并发请求共享一次抓取。
redirect_policy = (
f"{_LOGIN_WALLPAPER_MEDIA_PURPOSE}:{source_origin[0]}://"
f"{source_origin[1]}:{source_origin[2]}"
if media_source
else _LOGIN_WALLPAPER_PUBLIC_PURPOSE
)
content = await ImageHelper().async_fetch_image_guarded(
url=source_url,
redirect_validator=is_safe_redirect,
redirect_policy=redirect_policy,
max_bytes=_LOGIN_WALLPAPER_MAX_BYTES,
use_cache=True,
)
if not content:
raise HTTPException(status_code=502, detail="Wallpaper unavailable")
etag = f'"{HashUtils.md5(content)}"'
headers = RequestUtils.generate_cache_headers(etag, max_age=86400)
if RequestUtils.if_none_match_matches(if_none_match, etag):
return Response(status_code=304, headers=headers)
return Response(
content=content,
media_type=UrlUtils.get_mime_type(source_url, "image/jpeg"),
headers=headers,
)
return WallpaperHelper().get_wallpapers()