Files
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

277 lines
7.6 KiB
Go

package limitHandler
import (
"errors"
"fmt"
"strconv"
"sync"
"time"
"91porn-server/app/appg"
"91porn-server/app/service/m3u8ticket"
"91porn-server/common"
"91porn-server/common/constant"
"91porn-server/common/constant/redisconst"
"91porn-server/common/log"
"91porn-server/common/stderr"
"91porn-server/models/v/usermod"
"github.com/gin-gonic/gin"
)
type RequestCountTime struct {
//请求的词数
count int64
//最后的访问时间
lastTime int64
}
// 限制用户的每秒的请求的次数,1s10次的频率
func FilterRequest(ctx *gin.Context) {
uid, err := common.GetUID(ctx)
if err != nil {
common.ServeJSON(ctx, stderr.ErrNoToken, err)
return
}
key := redisconst.RechargeLimtKey(uid)
Expire := redisconst.RechargeLimtKeyExpire()
if appg.Redis.IsExist(key) {
common.ServeJSON(ctx, stderr.PayBusy, err)
ctx.Abort()
return
}
go func() { _ = appg.Redis.Set(key, "-", Expire) }()
}
// FilterRequestLimit 限制用户的每秒的请求的次数,1s1次的频率
func FilterRequestLimit(ctx *gin.Context) {
uid, err := common.GetUID(ctx)
if err != nil {
common.ServeJSON(ctx, stderr.ErrNoToken, err)
return
}
key := redisconst.ReqLimtKey(uid, ctx.Request.URL.Path)
Expire := redisconst.ReqLimtKeyExpire()
if appg.Redis.IsExist(key) {
common.ServeJSON(ctx, stderr.PayBusy, err)
ctx.Abort()
return
}
go func() { _ = appg.Redis.Set(key, "-", Expire) }()
}
// FilterRequestByUser 限制用户短时间内的请求次数,limit决定限制时间
func FilterRequestByUser(keyFmt string, limit time.Duration, errCode stderr.Code) gin.HandlerFunc {
return func(ctx *gin.Context) {
uid, err := common.GetUID(ctx)
if err != nil {
common.ServeJSON(ctx, stderr.ErrNoToken, err)
ctx.Abort()
return
}
key := fmt.Sprintf(keyFmt, uid)
if appg.Redis.IsExist(key) {
common.ServeJSON(ctx, errCode, err)
ctx.Abort()
return
}
go func() { _ = appg.Redis.Set(key, "-", limit) }()
}
}
// FilterSMSCaptchaByIP 限制单 IP 每自然日发送短信验证码的次数,超出 smsIPLimit 配置后拒绝
func FilterSMSCaptchaByIP(ctx *gin.Context) {
if !appg.ShouldEnforceIPRateLimit() {
return
}
ip := ctx.GetString(constant.CtxIP)
if ip == "" {
ip = ctx.ClientIP()
}
key := redisconst.SMSCaptchaIPKey(ip)
cnt := appg.Redis.Incr(key)
if cnt == 1 {
_, _ = appg.Redis.ExpireKey(key, redisconst.SMSCaptchaIPExpire())
}
limit := appg.Conf.Limit.SMSIPLimit
if limit <= 0 {
limit = 10
}
if cnt > limit {
log.Warn("sms captcha ip rate limit exceeded", log.Any("ip", ip))
common.ServeJSON(ctx, stderr.VisitLimit, nil)
ctx.Abort()
return
}
}
// FilterByIP wraps an IP-based Gin limiter and bypasses it only in the test
// environment. Other environments retain the original fail-safe behavior.
func FilterByIP(handler gin.HandlerFunc) gin.HandlerFunc {
return func(ctx *gin.Context) {
if !appg.ShouldEnforceIPRateLimit() {
return
}
handler(ctx)
}
}
// FilterSMSCaptchaByUID 限制单用户发送短信验证码:1分钟内只能发1次,每自然日最多5次
func FilterSMSCaptchaByUID(ctx *gin.Context) {
uid, err := common.GetUID(ctx)
if err != nil {
common.ServeJSON(ctx, stderr.ErrNoToken, err)
ctx.Abort()
return
}
// 1分钟冷却:key 存在则拒绝
cooldownKey := redisconst.SMSCaptchaUIDCooldownKey(uid)
if appg.Redis.IsExist(cooldownKey) {
log.Warn("sms captcha uid cooldown", log.Any("uid", uid))
common.ServeJSON(ctx, stderr.VisitLimit, nil)
ctx.Abort()
return
}
// 每日上限:计数器超过 5 次拒绝
dailyKey := redisconst.SMSCaptchaUIDKey(uid)
cnt := appg.Redis.Incr(dailyKey)
if cnt == 1 {
_, _ = appg.Redis.ExpireKey(dailyKey, redisconst.SMSCaptchaIPExpire())
}
if cnt > 5 {
log.Warn("sms captcha uid daily limit exceeded", log.Any("uid", uid))
common.ServeJSON(ctx, stderr.VisitLimit, nil)
ctx.Abort()
return
}
// 通过后写入冷却 key,1分钟内不能再发
go func() { _ = appg.Redis.Set(cooldownKey, "-", redisconst.SMSCaptchaUIDCooldown) }()
}
func FilterRequestByUserAndTerminal(keyFmt string, limit time.Duration, errCode stderr.Code) gin.HandlerFunc {
return func(ctx *gin.Context) {
ua, err := common.GetUA(ctx)
if err != nil {
common.ServeJSON(ctx, stderr.BadUA, err)
ctx.Abort()
return
}
if ua.IsH5 == "1" {
return
}
uid, err := common.GetUID(ctx)
if err != nil {
common.ServeJSON(ctx, stderr.ErrNoToken, err)
ctx.Abort()
return
}
key := fmt.Sprintf(keyFmt, uid)
if appg.Redis.IsExist(key) {
common.ServeJSON(ctx, errCode, err)
ctx.Abort()
return
}
go func() { _ = appg.Redis.Set(key, "-", limit) }()
}
}
const (
limitCount = 7
limitExpire = 3
mapLength = 5000
)
var mu = sync.Mutex{}
var limitMap = make(map[string]*RequestCountTime)
func Limit(ctx *gin.Context) {
uid, err := common.GetUID(ctx)
if err != nil {
common.ServeJSON(ctx, stderr.ErrNoToken, err)
return
}
key := ctx.Request.RequestURI + strconv.FormatInt(int64(uid), 10)
t1 := time.Now()
mu.Lock()
defer mu.Unlock()
if len(limitMap) > mapLength {
limitMap = make(map[string]*RequestCountTime)
}
v, ok := limitMap[key]
if !ok {
limitMap[key] = &RequestCountTime{
lastTime: t1.Unix(),
}
v = limitMap[key]
}
if t1.Unix() > v.lastTime+limitExpire {
//超出计时周期
v.lastTime = t1.Unix()
v.count = 0
}
v.count++
if v.count > limitCount {
log.Warn("abnormal user request too frequent", log.Any("uid", uid))
err = errors.New("user request too frequent")
common.ServeJSON(ctx, stderr.ErrNoToken, err)
return
}
}
const (
m3u8HourlyFreeLimit = 1800 // 普通用户每小时 m3u8 请求上限
m3u8HourlyVipLimit = 3600 // 会员每小时 m3u8 请求上限
// 命中限流时下发的提示 m3u8(替换 *source,不报错):普通用户与会员分别下发不同提示片
m3u8LimitFallbackNormal = "/bktadminup/sp/zp/hb/7a/0q/860513c4947143b29083160ec41588b5.m3u8" // 普通用户
m3u8LimitFallbackVip = "/bktadminup/sp/26/gx/7n/cr/359754435033419a95036ab33e520719.m3u8" // 会员
)
// M3u8HourlyLimit 限制每个用户每小时请求 m3u8 的次数:普通用户 50 次,会员 200 次。
// 超过免费次数后仅会员可继续,且总量不超过会员上限。
func M3u8HourlyLimit(ctx *gin.Context) {
uid, err := common.GetUID(ctx)
if err != nil || uid == 0 {
return // 取不到用户则不限流(交由上游鉴权处理)
}
key := redisconst.M3u8HourlyCountKey(uid)
cnt := appg.Redis.Incr(key)
if cnt == 1 {
_, _ = appg.Redis.ExpireKey(key, redisconst.M3u8HourlyCountExpire)
}
if cnt <= m3u8HourlyFreeLimit {
return
}
isVip := isM3u8VipUser(uid)
// 命中限流:非会员超普通上限、或会员超会员上限。不报错,改写 *source 为对应提示 m3u8 交后续 handler 下发
if !isVip || cnt > m3u8HourlyVipLimit {
log.Warn("m3u8 hourly rate limit exceeded", log.Any("uid", uid), log.Any("count", cnt), log.Any("vip", isVip))
fallback := m3u8LimitFallbackNormal
if isVip {
fallback = m3u8LimitFallbackVip
}
// 按最新规则(带票 + JHA 前缀)签发提示片地址,使严格验票路由(h5/m3u8)能识别并下发对应提示片,
// 不被验票逻辑当作非法票据覆盖;未开启票据时 BuildSignedURL 返回空,退回明文兜底(此时下游也不验票)。
ua := ""
if u, uaErr := common.GetUA(ctx); uaErr == nil {
ua = u.UserAgent
}
if signed := m3u8ticket.BuildSignedURL(uid, fallback, common.GetIP(ctx), ua, true, false); signed != "" {
fallback = signed
}
for i := range ctx.Params {
if ctx.Params[i].Key == "source" {
ctx.Params[i].Value = fallback
}
}
return
}
}
func isM3u8VipUser(uid uint64) bool {
u, err := usermod.FindUserByUID(uid)
if err != nil || u == nil {
return false
}
return u.IsVIP(time.Now())
}