Files
MeBox/internal/handler/dlna.go
T
2026-09-05 12:34:17 +08:00

82 lines
2.6 KiB
Go

// Package handler — DLNA / UPnP discovery + cast endpoints.
package handler
import (
"net/http"
"net/url"
"strings"
"github.com/gin-gonic/gin"
"github.com/truewhile/MeBox/internal/service"
)
func dlnaListHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
force := c.Query("force") == "true"
devices, err := svc.DLNA.Discover(c.Request.Context(), force)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"devices": devices})
}
}
type dlnaCastReq struct {
ControlURL string `json:"control_url" binding:"required"`
MediaURL string `json:"media_url" binding:"required"`
}
func dlnaCastHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
var req dlnaCastReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// SSRF 防护:control_url 必须命中本服务发现到的真实渲染设备,
// 防止登录用户借 cast 接口向任意内网地址发起 POST。
// 优先用 30s 缓存;未命中时强制重扫一次再校验(设备可能刚上线)。
devices, err := svc.DLNA.Discover(c.Request.Context(), false)
if err == nil && !dlnaControlURLKnown(devices, req.ControlURL) {
devices, err = svc.DLNA.Discover(c.Request.Context(), true)
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if !dlnaControlURLKnown(devices, req.ControlURL) {
c.JSON(http.StatusBadRequest, gin.H{"error": "unknown DLNA device: control_url must come from /api/dlna discovery"})
return
}
if err := svc.DLNA.Cast(c.Request.Context(), req.ControlURL, req.MediaURL); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusNoContent)
}
}
// dlnaControlURLKnown 判断 control_url 是否属于发现列表中的设备。
// 按解析后的 host:port+path 精确比对,容忍大小写与尾斜杠差异。
func dlnaControlURLKnown(devices []service.DLNADevice, controlURL string) bool {
want, err := url.Parse(strings.TrimSpace(controlURL))
if err != nil || want.Host == "" {
return false
}
for _, dev := range devices {
for _, candidate := range []string{dev.ControlURL, dev.Location} {
u, err := url.Parse(strings.TrimSpace(candidate))
if err != nil || u.Host == "" {
continue
}
if strings.EqualFold(u.Host, want.Host) &&
strings.EqualFold(strings.TrimRight(u.Path, "/"), strings.TrimRight(want.Path, "/")) {
return true
}
}
}
return false
}