fix(auth/images): long-lived Emby token, unthrottle refresh, serve library posters

Three regressions reported on third-party clients and the web UI:

- Third-party clients (Emby/Jellyfin) dropped login / could not play /
  could not refresh the library, roughly hourly. The Emby
  AuthenticateByName response returned the 60-minute access token, but
  Emby clients have no refresh mechanism and reuse the AccessToken until
  logout. Issue a long-lived (30d) token for the Emby compat layer via
  AuthService.IssueEmbyToken so device sessions persist.

- Web could be thrown back to login under load: /auth/refresh was inside
  the IP rate-limited /auth group, so multiple users/tabs behind one
  reverse-proxy/NAT IP exhausted the budget and refresh failed -> logout.
  Only login/register are rate-limited now (raised to 30/min for shared
  IPs); refresh is excluded (already protected by a one-time refresh token).

- Posters/images stopped displaying on the web home and other pages
  (refresh did not help). The SSRF/path hardening (a) blocked the image
  proxy whenever a hostname *resolved* to a private IP, which happens
  under GFW DNS poisoning of public CDNs like image.tmdb.org, and (b)
  restricted local image reads to data/cache/movies/tv/anime dirs only,
  dropping sidecar posters stored under arbitrary per-library roots to a
  placeholder. isPrivateHost now only blocks literal private/loopback IPs
  (real SSRF vectors) and ImageProxy also allows reads under configured
  library roots.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
soldosluka857
2026-05-30 07:08:29 +00:00
committed by Shuke
parent 93c9cb7dfb
commit 7cc59f095c
7 changed files with 215 additions and 25 deletions
+11 -5
View File
@@ -25,18 +25,24 @@ func Register(r *gin.Engine, cfg *config.Config, log *zap.Logger, svc *service.C
// Telegram Bot webhook — called by Telegram servers, no auth.
api.POST("/telegram/webhook", telegramWebhookHandler(svc))
// Rate limiter for auth endpoints: 10 attempts per minute per IP.
authLimiter := middleware.NewRateLimiter(10, 1*time.Minute)
// Rate limiter for credential endpoints (login/register): brute-force
// protection. 30/min per IP tolerates many users behind a single NAT
// or reverse-proxy IP while still throttling password guessing.
authLimiter := middleware.NewRateLimiter(30, 1*time.Minute)
// Public auth.
auth := api.Group("/auth")
auth.Use(middleware.RateLimit(authLimiter))
{
auth.POST("/login", loginHandler(svc))
auth.POST("/register", registerHandler(svc))
auth.POST("/login", middleware.RateLimit(authLimiter), loginHandler(svc))
auth.POST("/register", middleware.RateLimit(authLimiter), registerHandler(svc))
// /auth/refresh 用 RefreshHandler.RefreshToken:它从 body 读
// refresh_token 并签发新 access/refresh 对。旧的 refreshHandler
// 依赖 AuthRequired 中间件,永远 401,因此弃用。
//
// 刷新端点【不】做 IP 限流:刷新本身就是防止掉登录的机制,且已
// 由一次性轮换的 refresh token 强校验。若按 IP 限流,多个用户/
// 标签页共用一个反代 IP 时会把正常刷新打成 429,反而导致频繁
// 掉登录。
refreshHd := NewRefreshHandler(svc, log)
auth.POST("/refresh", refreshHd.RefreshToken)
}