@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// 其他关键监控指标
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package requestEncrypt
|
||||
@@ -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-----
|
||||
@@ -0,0 +1,6 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA/uHe9bW4vY5QdZHxLYm69zP6
|
||||
Ya5syB7VT6Ii5XDpoV9WdvVrB6WQxAPwePKdPZLENxEK1SX6N13tSVfCjNFIiPEX
|
||||
KAP50FHSQZxTQrdsR9S1ymBWB9ISJeZ4XsFLJ6/LXQldGh8zztUN3UhdZkOmKyb0
|
||||
1Vrs+QQc9zYrFNeJDQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user