160 lines
4.9 KiB
Go
160 lines
4.9 KiB
Go
package m3u8ticket
|
|
|
|
import (
|
|
"reflect"
|
|
|
|
"91porn-server/common"
|
|
"91porn-server/models/v/vidmod"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// maxSignDepth 反射兜底遍历响应体的最大深度,防止异常对象图导致过深递归(视频响应本身是浅树)。
|
|
const maxSignDepth = 8
|
|
|
|
// ticketSigner 实现 vidmod.M3u8Signer:持有本次请求的签票上下文(用户、客户端 IP/UA),
|
|
// 由各响应体的 SignM3u8 回调,对交出的每个 m3u8 地址字段就地签票。
|
|
type ticketSigner struct {
|
|
userID uint64
|
|
ip string
|
|
ua string
|
|
}
|
|
|
|
// SignM3u8URL 对单个 m3u8 地址字段就地签票;空串或签发失败(绝对地址、非 m3u8 等)时保持原值不变。
|
|
func (s *ticketSigner) SignM3u8URL(field *string, playable, preview bool) {
|
|
if field == nil || *field == "" {
|
|
return
|
|
}
|
|
if signed := BuildSignedURL(s.userID, *field, s.ip, s.ua, playable, preview); signed != "" {
|
|
*field = signed
|
|
}
|
|
}
|
|
|
|
func requestIP(c *gin.Context) string {
|
|
if c == nil {
|
|
return ""
|
|
}
|
|
return common.GetIP(c)
|
|
}
|
|
|
|
func requestUA(c *gin.Context) string {
|
|
if c == nil {
|
|
return ""
|
|
}
|
|
u, err := common.GetUA(c)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return u.UserAgent
|
|
}
|
|
|
|
// Sign 是 SignResponse 的便捷封装:自动从上下文取登录用户ID,控制器只需在返回前加一行
|
|
// m3u8ticket.Sign(ctx, data) 即可对响应体里的视频 m3u8 地址按需签票。
|
|
func Sign(c *gin.Context, data interface{}) {
|
|
if c == nil {
|
|
return
|
|
}
|
|
SignResponse(c, common.TryGetUID(c), data)
|
|
}
|
|
|
|
// SignResponse 是各视频接口的统一入口:对响应体 data 里的视频 m3u8 地址就地签票(不再限 H5)。
|
|
//
|
|
// data 只要实现 vidmod.M3u8Signable(所有携带视频地址的响应体都已实现)即可零反射签票;另外兼容控制器直接
|
|
// 下发的裸切片(recommser.GetAd 等)与 gin.H(RecommendList 等)。少数尚未实现接口的历史/边缘类型走反射兜底,
|
|
// 保证不漏签。data 为 nil / userID 为 0 / 未配置密钥(BuildSignedURL 返回空)时零副作用,兼容灰度未配置密钥。
|
|
func SignResponse(c *gin.Context, userID uint64, data interface{}) {
|
|
if data == nil || userID == 0 {
|
|
return
|
|
}
|
|
// data 可能是服务出错时返回的类型化 nil 指针((*Resp)(nil)):接口本身非 nil,但对值接收者方法会解引用
|
|
// nil 而 panic,先挡掉(等价于旧反射对 nil 指针的 IsNil 保护)。
|
|
if rv := reflect.ValueOf(data); rv.Kind() == reflect.Ptr && rv.IsNil() {
|
|
return
|
|
}
|
|
signData(&ticketSigner{userID: userID, ip: requestIP(c), ua: requestUA(c)}, data)
|
|
}
|
|
|
|
// signData 把 data 分派给对应签票实现:优先零反射的接口/裸切片/map,其余历史类型走反射兜底。
|
|
func signData(s vidmod.M3u8Signer, data interface{}) {
|
|
switch d := data.(type) {
|
|
case nil:
|
|
return
|
|
case vidmod.M3u8Signable:
|
|
d.SignM3u8(s)
|
|
case []*vidmod.VideoInfo:
|
|
vidmod.SignM3u8Infos(s, d)
|
|
case []*vidmod.VideoInfoResp:
|
|
vidmod.SignM3u8Resps(s, d)
|
|
case gin.H:
|
|
for _, v := range d {
|
|
signData(s, v)
|
|
}
|
|
default:
|
|
signByReflect(s, reflect.ValueOf(data), 0)
|
|
}
|
|
}
|
|
|
|
// signByReflect 是反射兜底:仅用于尚未实现 vidmod.M3u8Signable 的历史/边缘响应类型,语义与旧实现一致——
|
|
// 遍历对象图,命中 *VideoInfo/*VideoInfoResp 即签票。正常高频接口都走上面的接口分支,不会到这里。
|
|
// 待剩余类型补齐 SignM3u8 后,可整体删除本函数及 reflect 依赖。
|
|
func signByReflect(s vidmod.M3u8Signer, rv reflect.Value, depth int) {
|
|
if depth > maxSignDepth || !rv.IsValid() {
|
|
return
|
|
}
|
|
switch rv.Kind() {
|
|
case reflect.Ptr:
|
|
if rv.IsNil() {
|
|
return
|
|
}
|
|
switch v := rv.Interface().(type) {
|
|
case *vidmod.VideoInfo:
|
|
v.SignM3u8(s)
|
|
return
|
|
case *vidmod.VideoInfoResp:
|
|
v.SignM3u8(s)
|
|
return
|
|
}
|
|
signByReflect(s, rv.Elem(), depth+1)
|
|
case reflect.Interface:
|
|
if rv.IsNil() {
|
|
return
|
|
}
|
|
signByReflect(s, rv.Elem(), depth+1)
|
|
case reflect.Struct:
|
|
if rv.CanAddr() {
|
|
switch v := rv.Addr().Interface().(type) {
|
|
case *vidmod.VideoInfo:
|
|
v.SignM3u8(s)
|
|
return
|
|
case *vidmod.VideoInfoResp:
|
|
v.SignM3u8(s)
|
|
return
|
|
}
|
|
}
|
|
t := rv.Type()
|
|
for i := 0; i < rv.NumField(); i++ {
|
|
if t.Field(i).PkgPath != "" { // 跳过未导出字段,reflect 无法安全访问
|
|
continue
|
|
}
|
|
signByReflect(s, rv.Field(i), depth+1)
|
|
}
|
|
case reflect.Slice, reflect.Array:
|
|
for i := 0; i < rv.Len(); i++ {
|
|
signByReflect(s, rv.Index(i), depth+1)
|
|
}
|
|
case reflect.Map:
|
|
for _, k := range rv.MapKeys() {
|
|
signByReflect(s, rv.MapIndex(k), depth+1)
|
|
}
|
|
}
|
|
}
|
|
|
|
// SignURL 供只有单个 m3u8 地址字段的响应(官网视频、媒体内容等)复用。
|
|
// 不再限制 H5:已配置密钥时改写,未配置(BuildSignedURL 返回空)则原样返回,兼容灰度未配置密钥的情况。
|
|
func SignURL(c *gin.Context, userID uint64, field *string, playable, preview bool) {
|
|
if field == nil || *field == "" || userID == 0 {
|
|
return
|
|
}
|
|
(&ticketSigner{userID: userID, ip: requestIP(c), ua: requestUA(c)}).SignM3u8URL(field, playable, preview)
|
|
}
|