Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
@@ -0,0 +1,55 @@
package activityauth
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"io"
"net/http"
"91porn-server/app/appg"
"91porn-server/common/log"
"github.com/gin-gonic/gin"
)
// HmacAuth 活动服回调 HMAC-SHA256 签名校验中间件
func HmacAuth(c *gin.Context) {
signature := c.GetHeader("X-Signature")
if signature == "" {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "missing signature"})
c.Abort()
return
}
bodyBytes, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "read body failed"})
c.Abort()
return
}
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
secretKey := appg.Conf.ActivityServer.SecretKey
keyBytes, err := base64.StdEncoding.DecodeString(secretKey)
if err != nil {
log.Error("activityauth: decode secretKey failed", log.E(err))
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": "server config error"})
c.Abort()
return
}
h := hmac.New(sha256.New, keyBytes)
h.Write(bodyBytes)
expected := hex.EncodeToString(h.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(signature)) {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "invalid signature"})
c.Abort()
return
}
c.Next()
}
+265
View File
@@ -0,0 +1,265 @@
package authuser
import (
"net/http"
"strings"
"91porn-server/app/appg"
"91porn-server/common"
"91porn-server/common/constant"
"91porn-server/common/constant/redisconst"
"91porn-server/common/crypt"
"91porn-server/common/log"
"91porn-server/common/stderr"
"91porn-server/models/v/usermod"
"github.com/gin-gonic/gin"
)
var (
// 白名单,不需要验证token的api
// 在新的Auth中间件逻辑下,配置了白名单后,用户未登录,context中的uid会被设为0;否则,会绑定用户真实uid。
// 需要白名单中API的handler自己对用户登录状态进行判断。
whitelist = map[string]bool{
"/swagger": true,
"/api/app/mine/login": true,
"/api/app/im/whiteSign": true,
"/api/app/im/sign": true,
"/api/app/mine/mobileLoginOnly": true,
//"/api/app/ping/domain": true,
"/api/app/mine/ver": true,
"/api/app/ping/check": true,
"/api/app/static/faq/html/index": true,
"/api/app/static/faq/tmpl/index": true,
//"/api/app/vid/m3u8": true, //前端测试 下载m3u8 添加到白名单 ,生产环境中需要从白名单中剔除
"/api/app/vid/h5/light/m3u8": true, // 现有 H5 轻量播放接口
// H.265 云转码使用 Handler 自己的路径绑定、限时 HMAC 鉴权。
"/api/app/vid/transcode/m3u8": true,
"/sources": true,
"/defray/callback/shark": true,
"/defray/callback/goldfish": true,
"/api/app/vid/sec": true,
"/api/app/vid/pms/sec": true,
"/api/app/vid/pms/mt_sec": true,
"/api/app/vid/lsjsec": true,
"/api/app/vid/m3u8sec": true,
//"/api/app/notification/captcha": true,
"/api/app/newactivity": true,
"/api/app/sge": true,
"/api/app/code/webexchange": true,
"/api/app/game/userInfo": true,
"/api/app/game/deduct": true,
"/api/app/game/code": true,
"/api/app/userinvite/callback/recharge": true,
"/api/app/statcenter/sync": true,
"/api/app/avcomment/info": true,
"/api/app/ping/v": true,
//"/api/app/recommend": true,
// "/api/app/modules": true,
// "/api/app/tag/conf/list": true,
"/api/app/avcomment/list": true,
// 兼容登录、未登录状态的API
// "/api/app/vid/module/:subModuleID": true,
// "/api/app/vid/section/:sectionID": true,
"/api/app/mine/resetpassword": true,
"/api/app/mine/resetpassword/mobileverify": true,
"/api/app/mine/login/h5": true,
//"/api/app/comment/list": true,
//"/api/app/mine/info": true,
"/api/app/recommend/light/vids": true,
"/api/app/store_wallet": true,
"/api/app/aimate/sync": true,
"/api/app/health/ping": true,
}
)
func GetTokenSecret() string {
return appg.Conf.Base.JwtKey
}
type Claims struct {
UID uint64 `json:"uid"`
Timestamp int64 `json:"timestamp"`
Type uint8 `json:"type"`
}
func GenToken(claims *Claims) (string, error) {
secret := GetTokenSecret()
args, _ := common.JSONStruct2Map(claims)
token, err := crypt.CreateToken(secret, args)
if err != nil {
log.Error("genUserToken error", log.Any("claims", claims), log.E(err))
}
return token, err
}
func ParseToken(token string) (*Claims, error) {
secret := GetTokenSecret()
claims, err := crypt.ParseToken(secret, token)
if err != nil {
return nil, err
}
c := Claims{}
return &c, common.Map2JSONStruct(&c, claims)
}
func tokenRedisKey(uid uint64) string {
return redisconst.UserTokenKey(uid)
}
func cacheToken(uid uint64, token string) {
key := tokenRedisKey(uid)
_ = appg.Redis.Set(key, token, redisconst.UserTokenExpire)
}
func RevokeTokenCache(uids ...uint64) {
keys := make([]string, len(uids))
for i, uid := range uids {
keys[i] = tokenRedisKey(uid)
}
_, _ = appg.Redis.Del(keys...)
}
func auth(token string) (uint64, stderr.Code) {
claims, err := ParseToken(token)
if err != nil {
return 0, stderr.InvalidToken
}
uid := claims.UID
redisKey := tokenRedisKey(uid)
redisToken, err := appg.Redis.Get(redisKey)
if redisToken == nil || err != nil { //redis没有获取到token
u, err := usermod.FindUserByUID(uid)
if err != nil {
log.Warn("[InvalidToken] cause by find user by uid error", log.Any("uid", uid), log.E(err))
return 0, stderr.ErrNetWorkBusy
}
if u == nil {
log.Warn("[InvalidToken] cause by not find user info", log.Any("uid", uid))
return 0, stderr.UserIsNotExists
}
if u.Token != token {
log.Warn("[InvalidToken] cause by current token not equal user's token", log.Any("uid", uid), log.Any("user's token", u.Token), log.Any("current token", token))
return 0, stderr.InvalidToken
}
if u.HasLocked {
return uid, stderr.ErrAccessForbid
}
cacheToken(uid, u.Token)
} else {
if *redisToken != token {
log.Warn("[InvalidToken] cause by current token not equal redis's token", log.Any("uid", uid), log.Any("current token", token), log.Any("redis token", *redisToken))
return 0, stderr.InvalidToken
}
}
return uid, stderr.Success
}
func Auth(ctx *gin.Context) {
inWhiteList := false
for url, ok := range whitelist {
if ok && (strings.HasPrefix(ctx.Request.URL.Path, url) || strings.HasPrefix(ctx.FullPath(), url)) {
inWhiteList = true
break
}
}
var token string
var uid uint64
var code stderr.Code
t1 := ctx.Request.Header.Get("Authorization")
t2 := ctx.Query("token") //为兼容m3u8
if t1 != "" {
token = t1
}
if t2 != "" {
token = t2
}
if token == "" && !inWhiteList { // 请求的api未进入白名单且请求未带token,则视为非法请求,直接返回错误
ctx.AbortWithStatusJSON(http.StatusOK, stderr.ErrNoToken.Struct())
return
}
if token != "" { // 带token说明用户已登录,则解析并校验其token
uid, code = auth(token)
if code != stderr.Success {
// 白名单接口允许客户端携带失效token继续按未登录态访问,例如用户被删除后重新登录。
if inWhiteList {
ctx.Set(constant.CtxUserID, 0)
return
}
//兼容用户封禁时调用客服接口
if code == stderr.ErrAccessForbid && ctx.Request.URL.Path == "/api/app/im/newSign" {
ctx.Set(constant.CtxUserID, uid)
return
}
handleErrCode(ctx, uid, code)
return
}
ctx.Set(constant.CtxUserID, uid)
return
}
ctx.Set(constant.CtxUserID, 0)
}
// 处理返回的错误码
func handleErrCode(ctx *gin.Context, uid uint64, code stderr.Code) {
var data interface{}
switch code {
case stderr.ErrAccessForbid:
var reason string
u, err := usermod.FindUserByUID(uid)
if err == nil && u != nil {
reason = u.LockReason
}
data = gin.H{"uid": uid, "reason": reason}
}
common.ServeJSON(ctx, code, data)
ctx.Abort()
}
func getWebTokenSecret() string {
return appg.Conf.Base.WebJwtKey
}
// https://tools.ietf.org/html/rfc7519#section-4.1
// See examples for how to use this with your own claim types
type WebClaims struct {
//用户UID
UID uint64 `json:"uid,omitempty"`
//The "exp" (expiration time) claim identifies the expiration time on
//or after which the JWT MUST NOT be accepted for processing. The
//processing of the "exp" claim requires that the current date/time
//MUST be before the expiration date/time listed in the "exp" claim.
//Implementers MAY provide for some small leeway, usually no more than
//a few minutes, to account for clock skew. Its value MUST be a number
//containing a NumericDate value. Use of this claim is OPTIONAL.
ExpiresAt int64 `json:"exp,omitempty"`
//The "iat" (issued at) claim identifies the time at which the JWT was
//issued. This claim can be used to determine the age of the JWT. Its
//value MUST be a number containing a NumericDate value. Use of this
//claim is OPTIONAL.
IssuedAt int64 `json:"iat,omitempty"`
}
func GenWebToken(claims WebClaims) (string, error) {
secret := getWebTokenSecret()
m, _ := common.JSONStruct2Map(claims)
token, err := crypt.CreateToken(secret, m)
if err != nil {
log.Error("GenWebToken error", log.Any("claims", claims), log.E(err))
}
return token, err
}
// ParseWebClaims
// 如果token过期会返回error
func ParseWebClaims(token string) (WebClaims, error) {
secret := getWebTokenSecret()
claims, err := crypt.ParseToken(secret, token)
if err != nil {
return WebClaims{}, err
}
webClaims := WebClaims{}
return webClaims, common.Map2JSONStruct(&webClaims, claims)
}
+30
View File
@@ -0,0 +1,30 @@
package authuser
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func TestSignedTranscodePullEndpointDoesNotRequireUserToken(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(Auth)
router.GET("/api/app/vid/transcode/m3u8/*source", func(ctx *gin.Context) {
ctx.Status(http.StatusNoContent)
})
req := httptest.NewRequest(
http.MethodGet,
"/api/app/vid/transcode/m3u8/laosiji/m3m/source.m3u8",
nil,
)
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, req)
if recorder.Code != http.StatusNoContent {
t.Fatalf("transcode pull endpoint required an App token: status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
+123
View File
@@ -0,0 +1,123 @@
package datacenter
import (
"encoding/json"
"fmt"
"strings"
"time"
"91porn-server/app/appg"
"91porn-server/common"
"91porn-server/common/log"
"91porn-server/models/v/usermod"
"github.com/gin-gonic/gin"
)
type OnlineStatData struct {
UserId int64 `bson:"userId" json:"userId"` // 用户Id
DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码
WatchCount int64 `json:"watchCount,omitempty" bson:"watchCount"` // 观看次数
WatchTime int64 `json:"watchTime,omitempty" bson:"watchTime"` // 观看时长
AdClick int64 `json:"adClick,omitempty" bson:"adClick"` // 广告点击
AppClick int64 `json:"appClick,omitempty" bson:"appClick"` // APP点击
RequestCount int64 `json:"requestCount,omitempty" bson:"requestCount"` // 请求次数
RegisterAt time.Time `json:"registerAt,omitempty" bson:"registerAt"` // 注册时间(只统计最近一个月注册的渠道用户?)
StatIndex int64 `json:"statIndex" bson:"statIndex"` // 统计数据更新时钟
SendIndex int64 `json:"sendIndex" bson:"sendIndex"` // 上次发送时钟
SendAt time.Time `json:"sendAt" bson:"sendAt"` // 上次发送时间
}
// OnlineStat 统计用户在线时间和接口访问次数
func OnlineStat(ctx *gin.Context) {
now := time.Now()
var statInfo OnlineStatData
// 查询用户
userId, err := common.GetUID(ctx)
if userId <= 0 || err != nil {
return
}
userOnlineKey := fmt.Sprintf("mdsmOnline:%s:%d", now.Format("20060102"), userId)
onlineValue, err := appg.Redis.Get(userOnlineKey)
if err != nil {
log.Error("OnlineStat", log.E(err))
return
}
if onlineValue != nil && *onlineValue != "" {
err = json.Unmarshal([]byte(*onlineValue), &statInfo)
if err != nil {
log.Error("OnlineStat", log.E(err))
return
}
} else {
user, serr := usermod.FindUserByUID(userId)
if serr != nil {
return
}
statInfo = OnlineStatData{
UserId: int64(userId),
DistrictCode: user.DistrictCode,
RegisterAt: user.CreatedAt,
WatchCount: 0,
WatchTime: 0,
AdClick: 0,
AppClick: 0,
RequestCount: 0,
StatIndex: 0,
SendIndex: 0,
SendAt: now,
}
}
// 统计数据
StatApiHandle(ctx, &statInfo)
// 更新数据库埋点规则 () 后期只统计渠道用户
if (statInfo.StatIndex < 2) || //前1次直接发送
(statInfo.StatIndex-statInfo.SendIndex) > 50 || //每30次合并发送
statInfo.SendAt.Before(time.Now().Add(-time.Second*40)) { // 超过40秒
// 清理数据
statInfo.WatchCount = 0
statInfo.AdClick = 0
statInfo.AppClick = 0
statInfo.RequestCount = 0
statInfo.SendAt = now
statInfo.SendIndex = statInfo.StatIndex
}
marshalStr, err := json.Marshal(statInfo)
if err != nil {
log.Error("OnlineStat", log.E(err))
return
}
_ = appg.Redis.Set(userOnlineKey, marshalStr, 24*time.Hour)
}
// StatApiHandle 统计Api和关键接口次数
func StatApiHandle(ctx *gin.Context, statInfo *OnlineStatData) {
statInfo.StatIndex += 1
statInfo.RequestCount += 1
// 播放接口
if strings.HasPrefix(ctx.Request.URL.Path, "/api/app/vid/h5/m3u8/") ||
strings.HasPrefix(ctx.Request.URL.Path, "/api/app/vid/m3u8/") ||
strings.HasPrefix(ctx.Request.URL.Path, "/api/app/vid/h5/light/m3u8/") {
statInfo.WatchCount += 1
statInfo.StatIndex += 1
}
// 广告点击
if strings.Contains(ctx.Request.URL.Path, "/api/app/ads/click") ||
strings.Contains(ctx.Request.URL.Path, "/api/app/recreation/click") {
statInfo.AdClick += 1
statInfo.StatIndex += 1
}
// 其他关键监控指标
}
+13
View File
@@ -0,0 +1,13 @@
package ip
import (
"91porn-server/common/constant"
"github.com/gin-gonic/gin"
)
// RealIP 获取真实IP
func RealIP(c *gin.Context) {
ip := c.ClientIP()
c.Set(constant.CtxIP, ip)
}
+103
View File
@@ -0,0 +1,103 @@
package ipblock
import (
"net/http"
"time"
"91porn-server/app/appg"
"91porn-server/common"
"91porn-server/common/constant"
"91porn-server/common/constant/redisconst"
"91porn-server/common/stderr"
"91porn-server/models/v/ipblockmod"
"github.com/gin-gonic/gin"
)
func IPBlock(blockType string) gin.HandlerFunc {
return func(ctx *gin.Context) {
if !appg.ShouldEnforceIPRateLimit() {
return
}
ip := common.GetIP(ctx)
key := redisconst.IPBlockKey(blockType)
if count, err := appg.Redis.SCard(key); count == 0 || err != nil {
ipdata, err := ipblockmod.AggregateForIPArray(blockType)
if err == nil && len(ipdata.IPS) > 0 {
// 仅在集合为空时回源重建:直接 SAdd,去掉 Del 以消除"先删后加"的空窗期漏拦;
// TTL 设在 key 自身(此前误用 IPBlockKey(key) 二次包裹成 ip:block:ip:block:xxx
// 导致真实集合永不过期、永不回源刷新)
_, _ = appg.Redis.SAdd(key, ipdata.IPS)
_, _ = appg.Redis.ExpireKey(key, redisconst.IPBlockExpire)
}
}
isBlock, err := appg.Redis.SISMember(key, ip)
//表示当前IP在限制名单中
if isBlock && err == nil {
ctx.AbortWithStatusJSON(http.StatusOK, gin.H{
"code": stderr.ErrReqForbidden,
"msg": stderr.ErrReqForbidden.Msg(),
"tip": stderr.ErrReqForbidden.Tip(),
})
return
}
}
}
func IPAutoBlock(blockType string, duration time.Duration, count int64) gin.HandlerFunc {
return func(ctx *gin.Context) {
if !appg.ShouldEnforceIPRateLimit() {
return
}
ip := common.GetIP(ctx)
if ip != "" {
key := redisconst.IPAutoBlockKey(blockType, ip)
cnt := appg.Redis.Incr(key)
if cnt == 1 {
_, _ = appg.Redis.ExpireKey(key, duration)
}
if cnt >= count {
switch blockType {
case constant.Register:
ctx.AbortWithStatusJSON(http.StatusOK, gin.H{
"code": stderr.ErrLoginTooFrequently,
"msg": stderr.ErrLoginTooFrequently.Msg(),
"tip": stderr.ErrLoginTooFrequently.Tip(),
})
default:
ctx.AbortWithStatusJSON(http.StatusOK, gin.H{
"code": stderr.ErrReqForbidden,
"msg": stderr.ErrReqForbidden.Msg(),
"tip": stderr.ErrReqForbidden.Tip(),
})
}
return
}
}
}
}
func CountAutoBlock(blockType string, duration time.Duration, count int64) gin.HandlerFunc {
return func(ctx *gin.Context) {
var key string
switch blockType {
case constant.BlockComment:
uid, err := common.GetUID(ctx)
if err == nil {
key = redisconst.AutoBlockKey(blockType, uid)
}
}
cnt := appg.Redis.Incr(key)
if cnt == 1 {
_, _ = appg.Redis.ExpireKey(key, duration)
}
if cnt > count {
ctx.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{
"code": stderr.ErrReqForbidden,
"msg": stderr.ErrReqForbidden.Msg(),
"tip": stderr.ErrReqForbidden.Tip(),
})
return
}
}
}
+276
View File
@@ -0,0 +1,276 @@
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())
}
+92
View File
@@ -0,0 +1,92 @@
package requestEncrypt
import (
"bytes"
_ "embed"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/url"
"strings"
"91porn-server/common/constant"
"91porn-server/common/crypt"
"91porn-server/common/log"
"91porn-server/middleware/ua"
"github.com/gin-gonic/gin"
)
var (
// 白名单,不需要验证token的api
// 在新的Auth中间件逻辑下,配置了白名单后,用户未登录,context中的uid会被设为0;否则,会绑定用户真实uid。
// 需要白名单中API的handler自己对用户登录状态进行判断。
whitelist = map[string]bool{
"/swagger": true,
"/api/app/vid/m3u8": true, //前端测试 下载m3u8 添加到白名单 ,生产环境中需要从白名单中剔除
}
)
//go:embed public.pem
var PubKey []byte
type Req struct {
Data string `json:"data" form:"data"`
Sign string `json:"sign" form:"sign"`
}
// ReqDecode 解密
func ReqDecode(ctx *gin.Context) {
var err error
val, exists := ctx.Get(constant.CtxUA)
if !exists {
return
}
for url, ok := range whitelist {
if ok && (strings.HasPrefix(ctx.Request.URL.Path, url) || strings.HasPrefix(ctx.FullPath(), url)) {
return
}
}
u, ok := val.(ua.UA)
if !ok {
return
}
if u.Terminal != constant.TerminalH5 && u.Terminal != constant.TerminalWeb { //h5 和纯web都加密
return
}
var data Req
if err = ctx.ShouldBind(&data); err != nil {
return
}
if data.Data == "" {
return
}
dataBaseBytes, _ := base64.StdEncoding.DecodeString(data.Data)
dataBytes, err := crypt.AesDecrypt(string(dataBaseBytes), "BxJand%xf5h3sycH")
if err != nil {
log.Warn("ReqDecode is error", log.Any("data", data.Data), log.Any("ua", u))
}
if ctx.Request.Method == "GET" {
m := make(map[string]interface{})
dec := json.NewDecoder(strings.NewReader(dataBytes))
dec.UseNumber() // 保留数字原始表示,避免 float64 精度丢失/科学计数法
_ = dec.Decode(&m)
values := url.Values{}
for k, v := range m {
// 解密后的 JSON 值可能是数字、布尔等非字符串类型,不能直接断言为 string,否则会 panic
switch val := v.(type) {
case string:
values.Add(k, val)
case nil:
values.Add(k, "")
default:
values.Add(k, fmt.Sprintf("%v", val))
}
}
ctx.Request.URL.RawQuery = values.Encode()
ctx.Request.Form = values
return
}
ctx.Request.Body = io.NopCloser(bytes.NewBuffer([]byte(dataBytes)))
}
+1
View File
@@ -0,0 +1 @@
package requestEncrypt
+16
View File
@@ -0,0 +1,16 @@
-----BEGIN PRIVATE KEY-----
MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAMD+4d71tbi9jlB1
kfEtibr3M/phrmzIHtVPoiLlcOmhX1Z29WsHpZDEA/B48p09ksQ3EQrVJfo3Xe1J
V8KM0UiI8RcoA/nQUdJBnFNCt2xH1LXKYFYH0hIl5nhewUsnr8tdCV0aHzPO1Q3d
SF1mQ6YrJvTVWuz5BBz3NisU14kNAgMBAAECgYBAIHDMtLf8+n8fHPGxQYBSL3GF
8I8UdipIln05OyOZfZVAFabWOWQ6BeeJL6btuFfb+rAe+VP1IBCFl6kha8jdzQjr
48mujwFKSwtFUHJukTbG/pgQWjnblSPcK9XStu5caiwGG9ehBdPgKurMvr/5w0NL
VtYr22fVlfpUjHTDwQJBAO71ziG1Goz2dy1lZvb6m1MHx2vsBbWnoFzGL6LQ+Mis
FB6/DkFS5ayyGkubqC9HLCzvUzl/uO/F9F9J704WIDsCQQDOwf+0m8a1WGzNnN4N
ApyfV3ZMyMgcoFAgyS4HJ3ODrkfKIp3BUAKMWo7DZDNQy0h8RuIx8J1aFMP8vmb1
e29XAkBOE2gvYUH0js7vuTMEPWuknGUPIQXYjZV62pJT611uC7NjPF+G/nPmkia/
T3OLRwDrCaMypUWiqhYnUDvtUpNjAkAoTLvwkiRrGG8srJTDaGzqyftu//uxSLYV
jeEA1/m2AvRPO+wPz+6PON92ykMOTnE0eASCw2mIM1/4FYyv8H1FAkAWhNRdcXbB
yuvFbO7xSsstrTlyq6AqHXfCwT+NYRsa7eAQucOMQUY+6zKXsZ2WzvoiN+y7MbqJ
VheW/ortGjBA
-----END PRIVATE KEY-----
@@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDA/uHe9bW4vY5QdZHxLYm69zP6Ya5syB7VT6Ii5XDpoV9WdvVr
B6WQxAPwePKdPZLENxEK1SX6N13tSVfCjNFIiPEXKAP50FHSQZxTQrdsR9S1ymBW
B9ISJeZ4XsFLJ6/LXQldGh8zztUN3UhdZkOmKyb01Vrs+QQc9zYrFNeJDQIDAQAB
AoGAQCBwzLS3/Pp/HxzxsUGAUi9xhfCPFHYqSJZ9OTsjmX2VQBWm1jlkOgXniS+m
7bhX2/qwHvlT9SAQhZepIWvI3c0I6+PJro8BSksLRVBybpE2xv6YEFo525Uj3CvV
0rbuXGosBhvXoQXT4CrqzL6/+cNDS1bWK9tn1ZX6VIx0w8ECQQDu9c4htRqM9nct
ZWb2+ptTB8dr7AW1p6Bcxi+i0PjIrBQevw5BUuWsshpLm6gvRyws71M5f7jvxfRf
Se9OFiA7AkEAzsH/tJvGtVhszZzeDQKcn1d2TMjIHKBQIMkuBydzg65HyiKdwVAC
jFqOw2QzUMtIfEbiMfCdWhTD/L5m9XtvVwJAThNoL2FB9I7O77kzBD1rpJxlDyEF
2I2VetqSU+tdbguzYzxfhv5z5pImv09zi0cA6wmjMqVFoqoWJ1A77VKTYwJAKEy7
8JIkaxhvLKyUw2hs6sn7bv/7sUi2FY3hANf5tgL0TzvsD8/ujzjfdspDDk5xNHgE
gsNpiDNf+BWMr/B9RQJAFoTUXXF2wcrrxWzu8UrLLa05cqugKh13wsE/jWEbGu3g
ELnDjEFGPusyl7Gdls76IjfsuzG6iVYXlv6K7RowQA==
-----END RSA PRIVATE KEY-----
+6
View File
@@ -0,0 +1,6 @@
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA/uHe9bW4vY5QdZHxLYm69zP6
Ya5syB7VT6Ii5XDpoV9WdvVrB6WQxAPwePKdPZLENxEK1SX6N13tSVfCjNFIiPEX
KAP50FHSQZxTQrdsR9S1ymBWB9ISJeZ4XsFLJ6/LXQldGh8zztUN3UhdZkOmKyb0
1Vrs+QQc9zYrFNeJDQIDAQAB
-----END PUBLIC KEY-----
+66
View File
@@ -0,0 +1,66 @@
package vercheck
import (
"strings"
"91porn-server/app/service/versionser"
"91porn-server/common"
"91porn-server/common/constant"
"91porn-server/common/log"
"91porn-server/common/stderr"
"91porn-server/common/version"
"91porn-server/models/v/versionmod"
"github.com/gin-gonic/gin"
)
// 检查版本更新
func VerCheck() gin.HandlerFunc {
return func(ctx *gin.Context) {
ua, err := common.GetUA(ctx)
if err != nil || ua.SysType == "" {
log.Warn("get ua error or us is empty", log.E(err))
return
}
var sys string
if strings.Contains(strings.ToLower(ua.SysType), constant.DeviceTypeAndroid) {
sys = constant.DeviceTypeAndroid
}
if strings.Contains(strings.ToLower(ua.SysType), constant.DeviceTypeIOS) {
sys = constant.DeviceTypeIOS
}
//查找最新版本信息
ver, err := versionmod.FindVersionExcluedBuildId(sys)
if err != nil || ver.ID.IsZero() {
log.Warn("check version get version error", log.E(err))
return
}
//最新 为强制更新 需要强制更新
vn, _ := version.New(ver.VersionName)
vc, _ := version.New(ua.Ver)
if vn.GT(vc) && versionser.HandleVersion(ua.Ver, &ver) && ver.ForcedUpdate {
common.ServeJSON(ctx, stderr.ErrVersionUpdate, ver)
ctx.Abort()
return
}
}
}
// VerH5Check 检查h5版本更新
func VerH5Check(ctx *gin.Context) {
ua, err := common.GetUA(ctx)
if err != nil {
log.Warn("get ua error", log.E(err))
return
}
if !strings.Contains(strings.ToLower(ua.SysType), constant.DeviceTypeH5) {
return
}
//查找最新版本信息
ver, err := versionmod.FindVersion(constant.DeviceTypeH5)
if err != nil {
log.Warn("check version get version error", log.E(err))
return
}
ctx.Writer.Header().Set("Cur-Ver", ver.VersionName)
}
+194
View File
@@ -0,0 +1,194 @@
package verfyparam
import (
"91porn-server/common/timeutil/timerange"
"strconv"
"strings"
"time"
"91porn-server/app/appg"
"91porn-server/common"
"91porn-server/common/constant"
"91porn-server/common/constant/redisconst"
"91porn-server/common/crypt"
"91porn-server/common/log"
"91porn-server/common/stderr"
"github.com/gin-gonic/gin"
)
var (
// 白名单,不需要验证token的api
whitelist = map[string]bool{
"/swagger": true,
"/api/app/ping/check": true,
"/api/app/ping/pass": true,
"/api/app/daichong": true,
"/api/app/vid/sec": true,
"/api/app/vid/new/sec": true,
"/api/app/vid/lsjsec": true,
// H.265 云转码使用 Handler 自己的路径绑定、限时 HMAC 鉴权。
"/api/app/vid/transcode/m3u8": true,
"/api/app/im/whiteSign": true,
"/api/app/im/sign": true,
"/api/app/vid/upload": true,
}
)
// 内部使用
const InnerSecret string = "F^hgNT%MBpai+3qkz05NZtB5Ts@a_gRekEHJ3@KcF)T5>5.U7JLbx3!P1nxt#LhV"
type Sign struct {
Nonce string `json:"nonce"`
TimeStamp string `json:"timestamp"`
Path string `json:"path"`
UserAgent string `json:"userAgent"`
Token string `json:"token"`
}
func VerifyReplayAttackRequest(replayAttack appg.ReplayAttackConfig) gin.HandlerFunc {
return func(ctx *gin.Context) {
if !replayAttack.Enable {
return
}
flag := common.IsGTESpecifyVer(ctx, constant.Ver3_6_0)
if !flag {
return
}
for url, ok := range whitelist {
if ok && strings.HasPrefix(ctx.Request.URL.Path, url) {
return
}
}
var sign, timestamp, nonce string
ua := ctx.Request.UserAgent()
token := ctx.GetHeader("Authorization")
apiKey := ctx.GetHeader("x-api-key")
timestamp, sign, nonce = getXapiKey(apiKey)
if sign == InnerSecret {
return
}
t, _ := strconv.ParseInt(timestamp, 10, 64)
if t == 0 || sign == "" || nonce == "" {
ctx.Abort()
common.ServeJSON(ctx, stderr.ErrInvalidRequestReplayAttack, nil)
log.Info("VerifyReplayAttackRequest bad request UA", log.Any("apiKey", apiKey), log.Any("ip", ctx.ClientIP()))
return
}
nowTime := time.Now().UTC().Unix()
if nowTime > t+replayAttack.WindowDurationSeconds || nowTime < t-replayAttack.WindowDurationSeconds {
ctx.Abort()
common.ServeJSON(ctx, stderr.ErrInvalidRequestReplayAttack, nil)
log.Info("VerifyReplayAttackRequest bad request time window", log.Any("apiKey", apiKey), log.Any("ip", ctx.ClientIP()))
return
}
redisKey := redisconst.ReplayNonceKey(nonce)
if appg.Redis.IsExist(redisKey) {
ctx.Abort()
common.ServeJSON(ctx, stderr.ErrInvalidRequestReplayAttack, nil)
log.Info("VerifyReplayAttackRequest bad request dup nonce", log.Any("apiKey", apiKey), log.Any("ip", ctx.ClientIP()))
return
}
s := Sign{
Nonce: nonce,
TimeStamp: timestamp,
Path: ctx.Request.URL.Path,
UserAgent: ua,
Token: token,
}
hmac := GeneratorSign(s, replayAttack.Key)
if hmac != sign {
ctx.Abort()
common.ServeJSON(ctx, stderr.ErrInvalidRequestReplayAttack, nil)
return
}
if err := appg.Redis.Set(redisKey, 1, 2*time.Duration(replayAttack.WindowDurationSeconds)*time.Second); err != nil {
log.Info("VerifyReplayAttackRequest Save Redis Err", log.Any("key", redisKey), log.E(err))
}
}
}
func CheckReplayAttackRequest() gin.HandlerFunc {
return func(ctx *gin.Context) {
if !appg.ShouldEnforceIPRateLimit() {
return
}
for url, ok := range whitelist {
if ok && strings.HasPrefix(ctx.Request.URL.Path, url) {
return
}
}
ua := ctx.Request.UserAgent()
ip := common.GetIP(ctx)
var now = time.Now()
var recentSecond = timerange.RecentSecond(now, 10)
checkApiKey := redisconst.CheckApiKey(ip, ctx.Request.URL.Path, recentSecond)
if ua == "" {
ctx.Abort()
common.ServeJSON(ctx, stderr.ErrInvalidRequestReplayAttack, nil)
log.InfoX(ctx, "CheckReplayAttackRequest bad request UA", log.Any("apiKey", checkApiKey), log.Any("ip", ip))
return
}
data, err := appg.Redis.Get(checkApiKey)
if err != nil {
log.InfoX(ctx, "CheckReplayAttackRequest get Redis Err", log.Any("key", checkApiKey), log.E(err))
}
if data == nil {
err := appg.Redis.Set(checkApiKey, "1", redisconst.CheckApiKeyExpire)
if err != nil {
log.InfoX(ctx, "CheckReplayAttackRequest Save Redis Err", log.Any("key", checkApiKey), log.E(err))
}
}
if data != nil {
currentValue, err := strconv.ParseInt(*data, 10, 64)
if err != nil {
log.InfoX(ctx, "CheckReplayAttackRequest strconv.ParseInt Err", log.Any("key", checkApiKey), log.E(err))
}
if currentValue > 10 {
ctx.Abort()
common.ServeJSON(ctx, stderr.ErrInvalidRequestReplayAttack, nil)
log.InfoX(ctx, "CheckReplayAttackRequest bad request", log.Any("currentValue", currentValue), log.Any("apiKey", checkApiKey), log.Any("ip", ip))
return
}
currentValue += 1
setData := strconv.FormatInt(currentValue, 10)
err = appg.Redis.Set(checkApiKey, setData, redisconst.CheckApiKeyExpire)
if err != nil {
log.InfoX(ctx, "CheckReplayAttackRequest set Redis Err", log.Any("key", checkApiKey), log.E(err))
}
}
ctx.Next()
}
}
func getXapiKey(apiKey string) (timestamp, sign, nonce string) {
if apiKey != "" {
apiKeys := strings.Split(apiKey, ";")
for _, v := range apiKeys {
vss := strings.Split(strings.TrimSpace(v), "=")
if len(vss) < 2 {
log.Warn("x-api-key miss", log.Any("x-api-key", apiKey))
continue
}
switch strings.TrimSpace(vss[0]) {
case "timestamp":
timestamp = strings.TrimSpace(vss[1])
case "sign":
sign = strings.TrimSpace(vss[1])
case "nonce":
nonce = strings.TrimSpace(vss[1])
default:
log.Warn("x-api-key miss", log.Any("x-api-key", apiKey))
}
}
}
return
}
func GeneratorSign(s Sign, sec string) string {
str, _ := crypt.StructToStr(s)
return crypt.StrToHmacSha1(str, sec)
}
@@ -0,0 +1,32 @@
package verfyparam
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func TestTranscodeM3u8BypassesAppReplayValidationWithoutUserAgent(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(CheckReplayAttackRequest())
router.GET("/api/app/vid/transcode/m3u8/*source", func(ctx *gin.Context) {
ctx.Status(http.StatusNoContent)
})
req := httptest.NewRequest(
http.MethodGet,
"/api/app/vid/transcode/m3u8/laosiji/m3m/source.m3u8",
nil,
)
req.Header.Del("User-Agent")
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, req)
if recorder.Code != http.StatusNoContent {
t.Fatalf("light m3u8 request was blocked: status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
+134
View File
@@ -0,0 +1,134 @@
package visitlog
import (
"91porn-server/app/service/adser"
"91porn-server/common/constant"
"context"
"time"
"91porn-server/app/appg"
"91porn-server/common"
"91porn-server/common/constant/redisconst"
"91porn-server/common/log"
"91porn-server/common/timeutil"
"91porn-server/middleware/ua"
"91porn-server/models/l/visitlogmod"
"91porn-server/models/v/usermod"
"github.com/gin-gonic/gin"
)
// LogMiddleware LogMiddleware
func Log(c *gin.Context) {
uid, err := common.GetUID(c)
if err != nil {
return
}
if uid == 0 {
// log.Info("uid is empty ") //
return
}
uas, err := common.GetUA(c)
if err != nil {
log.Warn("get ua error", log.E(err))
return
}
now := time.Now()
//统计访问频率
userVisitLogKey := redisconst.UserVisitLogsKey(now)
//2021-01-11去掉用户行为记录
//registerBehaKey := redisconst.RegistBehaviorKey(uid)
//每日访问日志
//common.Go(func() {
// redisVisit(uid, uas.SysType, now)
//})
var exists bool
if val, err := appg.Redis.GetBit(userVisitLogKey, int64(uid)); err == nil {
exists = val == 1
}
if !exists { //有值了就返回
_ = appg.Redis.SetBit(userVisitLogKey, int64(uid), 1)
_, _ = appg.Redis.ExpireKey(userVisitLogKey, redisconst.UserVisitLogExpire(now))
common.Go(func() {
ip := common.GetIP(c)
recordVisitLogFull(uid, ip, uas, now)
})
common.Go(func() {
user, err := usermod.FindUserByUID(uid)
if err != nil {
log.Warn("find user by uid error", log.E(err))
return
}
if user == nil {
return
}
_ = adser.UpsertAdStat(context.Background(), user, time.Now(), 0, 0, 0)
})
}
}
//func redisVisit(uid uint64, sysType string, now time.Time) {
// recentMinute := timerange.RecentMinute(now, statrecordmod.FiveMinuteScale)
// redisKey := redisconst.VisitKey(recentMinute)
// _, _ = appg.Redis.SAdd(redisKey, usermod.VisitValue{UID: uid, SysType: sysType}.JsonString())
// _, _ = appg.Redis.ExpireKey(redisKey, redisconst.VisitExpireMax)
//}
func recordVisitLog(uid uint64, ip string, ua ua.UA, now time.Time) {
res, err := visitlogmod.UpsertUserVisit(&visitlogmod.VisitLog{
SumDate: timeutil.BeginningOfDay(now),
UID: uid,
IP: ip,
SysType: common.HandleSysType(ua.SysType),
Ver: ua.Ver,
DevType: ua.DevType,
DevID: ua.DevID,
BuildID: ua.BuildID,
IsDeduction: true,
})
if err != nil {
log.Warn("recordVisitLog UpsertUserVisit", log.E(err))
return
}
if res != nil && res.UpsertedCount > 0 {
if err := usermod.ChangeVisit(uid, ua.Ver, ua.SysType, now); err != nil {
log.Warn("recordVisitLog UserSelector", log.E(err))
}
}
}
func recordVisitLogFull(uid uint64, ip string, ua ua.UA, now time.Time) {
u, _ := usermod.FindUserByUID(uid)
if u == nil {
recordVisitLog(uid, ip, ua, now)
return
}
res, err := visitlogmod.UpsertUserVisit(&visitlogmod.VisitLog{
SumDate: timeutil.BeginningOfDay(now),
UID: uid,
IP: ip,
SysType: common.HandleSysType(ua.SysType),
Ver: ua.Ver,
DevType: ua.DevType,
DevID: ua.DevID,
BuildID: ua.BuildID,
IsDirect: u.IsDirect,
DistrictCode: u.DistrictCode,
RegisterTime: u.CreatedAt,
IsDeduction: false,
})
if err != nil {
log.Warn("recordVisitLog UpsertUserVisit", log.E(err))
return
}
if res != nil && res.UpsertedCount > 0 {
if err := usermod.ChangeVisit(uid, ua.Ver, ua.SysType, now); err != nil {
log.Warn("recordVisitLog UserSelector", log.E(err))
}
}
// 游客信息不记录
if ua.Terminal == constant.TerminalWeb {
return
}
}
+28
View File
@@ -0,0 +1,28 @@
package watcher
import (
"91porn-server/common"
"91porn-server/common/constant"
"91porn-server/common/stderr"
"91porn-server/models/v/usermod"
"net/http"
"github.com/gin-gonic/gin"
)
// 异常用户处理
func WatcherUnusualUser() gin.HandlerFunc {
return func(ctx *gin.Context) {
uid, err := common.GetUID(ctx)
//检查当前用户注册得分情况
if uid != 0 && err == nil {
user, _ := usermod.FindUserByUIDForNoCache(uid)
if user != nil {
if user.TrueScore == -1 || (user.TrueScore <= constant.UserLowestTrueScore && user.TrueScore > 0) {
ctx.AbortWithStatusJSON(http.StatusForbidden, stderr.UserIsException.Struct())
return
}
}
}
}
}