277 lines
7.6 KiB
Go
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())
|
|
}
|