mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 19:46:38 +08:00
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:
@@ -207,8 +207,16 @@ func embyAuthByNameHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
userPayload, _ := svc.Emby.FindUser(c.Request.Context(), resp.User.ID)
|
||||
// Emby/Jellyfin 客户端没有 refresh token 机制:它们把这里返回的
|
||||
// AccessToken 长期保存并反复使用。若返回 60 分钟的普通 access
|
||||
// token,客户端每小时就会掉登录、无法播放、媒体库无法刷新。因此
|
||||
// 签发长期令牌(IssueEmbyToken)匹配 Emby 持久化令牌语义。
|
||||
accessToken := resp.Tokens.AccessToken
|
||||
if longLived, err := svc.Auth.IssueEmbyToken(resp.User); err == nil && longLived != "" {
|
||||
accessToken = longLived
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"AccessToken": resp.Tokens.AccessToken,
|
||||
"AccessToken": accessToken,
|
||||
"ServerId": "mediastation-go-001",
|
||||
"User": userPayload,
|
||||
"SessionInfo": gin.H{
|
||||
@@ -729,7 +737,9 @@ func registerEmbyRoutes(r *gin.Engine, jwtSecret string, svc *service.Container)
|
||||
grp.HEAD(path, embyPingHandler(svc))
|
||||
grp.POST(path, embyPingHandler(svc))
|
||||
}
|
||||
embyLoginLimiter := middleware.NewRateLimiter(10, 1*time.Minute)
|
||||
// 30/min per IP: many Emby clients sit behind a single NAT/reverse-proxy
|
||||
// IP, so a low limit would throttle legitimate logins into 429s.
|
||||
embyLoginLimiter := middleware.NewRateLimiter(30, 1*time.Minute)
|
||||
for _, path := range []string{"/Users/AuthenticateByName", "/users/authenticatebyname"} {
|
||||
grp.POST(path, middleware.RateLimit(embyLoginLimiter), embyAuthByNameHandler(svc))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user