@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user