11import asyncio
22import json
3- import posixpath
4- import re
53from collections import deque
64from datetime import datetime
75from typing import Any , Optional , Union , Annotated
8- from urllib .parse import parse_qs , unquote , urljoin , urlparse
6+ from urllib .parse import urljoin , urlparse
97
108import aiofiles
119import pillow_avif # noqa 用于自动注册AVIF支持
3230 get_current_active_user_async ,
3331)
3432from app .helper .image import ImageHelper
35- from app .helper .mediaserver import MediaServerHelper
3633from app .helper .message import MessageHelper
3734from app .helper .progress import ProgressHelper
3835from app .helper .rule import RuleHelper
5249router = APIRouter ()
5350
5451_NETTEST_REDIRECT_STATUS_CODES = {301 , 302 , 303 , 307 , 308 }
55- _MEDIA_SERVER_IMAGE_PATH_PATTERNS = (
56- re .compile (
57- r"^/(?:emby/)?Items/[^/]+/Images/"
58- r"(?:Primary|Art|Backdrop|Banner|Logo|Thumb|Disc|Box|Screenshot|Menu|Chapter)"
59- r"(?:/[^/]+)?$" ,
60- re .IGNORECASE ,
61- ),
62- re .compile (
63- r"^/library/metadata/[^/]+/"
64- r"(?:thumb|art|banner|poster|clearlogo|clearart|background)"
65- r"(?:/[^/]+)?$" ,
66- re .IGNORECASE ,
67- ),
68- re .compile (r"^/api/v1/sys/img/.+" , re .IGNORECASE ),
69- )
7052
7153
7254def _match_nettest_prefix (url : str , prefix : str ) -> bool :
@@ -359,103 +341,13 @@ async def _close_nettest_response(response: Any) -> None:
359341 logger .debug (f"关闭网络测试响应失败: { err } " )
360342
361343
362- def _normalize_proxy_image_path (path : str ) -> str :
363- """
364- 归一化代理图片路径,用于识别媒体服务器图片接口。
365-
366- URL path 可能包含编码后的特殊字符,这里先解码再规范化路径,避免
367- `%2e%2e` 或重复斜杠绕过后续的媒体图片路径判断。
368- """
369- decoded_path = unquote (path or "/" )
370- normalized_path = posixpath .normpath (decoded_path )
371- if not normalized_path .startswith ("/" ):
372- normalized_path = f"/{ normalized_path } "
373- return normalized_path
374-
375-
376- def _is_known_media_server_image_path (path : str ) -> bool :
377- """
378- 判断路径是否属于已知媒体服务器图片读取接口。
379-
380- 这里仅覆盖 MoviePilot 自身会返回给前端的封面、背景图和图片流接口,
381- 不允许媒体服务器同 host 下的任意 API 路径继续通过图片代理访问。
382- """
383- normalized_path = _normalize_proxy_image_path (path )
384- return any (
385- pattern .match (normalized_path )
386- for pattern in _MEDIA_SERVER_IMAGE_PATH_PATTERNS
387- )
388-
389-
390- def _is_plex_transcode_image_url (url : str ) -> bool :
391- """
392- 校验 Plex 图片转码接口只转码 Plex 自身 metadata 图片路径。
393-
394- Plex 的 posterUrl/artUrl 可能使用 `/photo/:/transcode` 包装真实图片路径,
395- 因此需要额外检查 query 里的 `url` 仍然是 metadata 图片路径,而不是
396- 任意可被 Plex 代取的地址。
397- """
398- parsed_url = urlparse (url )
399- if _normalize_proxy_image_path (parsed_url .path ) != "/photo/:/transcode" :
400- return False
401- source_path = parse_qs (parsed_url .query ).get ("url" , [None ])[0 ]
402- if not source_path :
403- return False
404- source_url = urlparse (source_path )
405- if source_url .scheme or source_url .netloc :
406- return False
407- return _is_known_media_server_image_path (source_path )
408-
409-
410- def _is_ugreen_image_stream_url (url : str ) -> bool :
411- """
412- 校验绿联本机图片流接口只代理官方 scraper 图片。
413-
414- 绿联本地图片需要带加密鉴权头,目前模块只会把 scraper.ugnas.com 的签名图
415- 转成 getImaStream,本检查避免用户把该接口改造成任意远程 URL 中转。
416- """
417- parsed_url = urlparse (url )
418- if _normalize_proxy_image_path (parsed_url .path ) != "/ugreen/v2/video/getImaStream" :
419- return False
420- source_url = parse_qs (parsed_url .query ).get ("name" , [None ])[0 ]
421- if not source_url :
422- return False
423- parsed_source = urlparse (source_url )
424- return (
425- parsed_source .scheme in {"http" , "https" }
426- and parsed_source .netloc .lower () == "scraper.ugnas.com"
427- )
428-
429-
430- def _is_allowed_media_server_image_url (
431- url : str ,
432- media_server_domains : set [str ],
433- ) -> bool :
434- """
435- 判断内网媒体服务器 URL 是否可作为图片代理目标。
436-
437- 私有地址默认仍然禁止;只有 URL host 精确命中已配置媒体服务器,并且路径是
438- 已知图片接口时才允许访问,用于兼容前端媒体库和最近入库图片展示。
439- """
440- if not media_server_domains :
441- return False
442- if not SecurityUtils .is_safe_url (url , media_server_domains , strict = True ):
443- return False
444- return (
445- _is_known_media_server_image_path (urlparse (url ).path )
446- or _is_plex_transcode_image_url (url )
447- or _is_ugreen_image_stream_url (url )
448- )
449-
450-
451344async def fetch_image (
452345 url : str ,
453346 proxy : Optional [bool ] = None ,
454347 use_cache : bool = False ,
455348 if_none_match : Optional [str ] = None ,
456349 cookies : Optional [str | dict ] = None ,
457350 allowed_domains : Optional [set [str ]] = None ,
458- media_server_domains : Optional [set [str ]] = None ,
459351) -> Optional [Response ]:
460352 """
461353 处理图片缓存逻辑,支持HTTP缓存和磁盘缓存
@@ -466,17 +358,16 @@ async def fetch_image(
466358 if allowed_domains is None :
467359 allowed_domains = set (settings .SECURITY_IMAGE_DOMAINS )
468360
361+ fetch_url = SecurityUtils .strip_url_signature (url )
469362 # 验证URL安全性
470363 if not SecurityUtils .is_safe_url (
471364 url , allowed_domains , block_private = True
472- ) and not _is_allowed_media_server_image_url (
473- url , media_server_domains or set ()
474- ):
365+ ) and not (fetch_url := SecurityUtils .verify_signed_url (url )):
475366 logger .warn (f"Blocked unsafe image URL: { url } " )
476367 return None
477368
478369 content = await ImageHelper ().async_fetch_image (
479- url = url ,
370+ url = fetch_url ,
480371 proxy = proxy ,
481372 use_cache = use_cache ,
482373 cookies = cookies ,
@@ -491,7 +382,7 @@ async def fetch_image(
491382 # 返回缓存图片
492383 return Response (
493384 content = content ,
494- media_type = UrlUtils .get_mime_type (url , "image/jpeg" ),
385+ media_type = UrlUtils .get_mime_type (fetch_url , "image/jpeg" ),
495386 headers = headers ,
496387 )
497388 return None
@@ -509,13 +400,6 @@ async def proxy_img(
509400 """
510401 图片代理,可选是否使用代理服务器,支持 HTTP 缓存
511402 """
512- # 媒体服务器添加图片代理支持
513- hosts = [
514- config .config .get ("host" )
515- for config in MediaServerHelper ().get_configs ().values ()
516- if config and config .config and config .config .get ("host" )
517- ]
518- media_server_domains = set (hosts )
519403 allowed_domains = set (settings .SECURITY_IMAGE_DOMAINS )
520404 cookies = (
521405 MediaServerChain ().get_image_cookies (server = None , image_url = imgurl )
@@ -529,7 +413,6 @@ async def proxy_img(
529413 cookies = cookies ,
530414 if_none_match = if_none_match ,
531415 allowed_domains = allowed_domains ,
532- media_server_domains = media_server_domains ,
533416 )
534417
535418
0 commit comments