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
+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())
}
}