Initial commit: web_hghl_server
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
package api_security
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func BodyDump() gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
// fmt.Println("=========================== 1")
|
||||
|
||||
// fmt.Printf("%+v\n", ctx.Request.Header)
|
||||
|
||||
// data, e := ioutil.ReadAll(ctx.Request.Body)
|
||||
// if e != nil {
|
||||
// ctx.AbortWithStatusJSON(500, "protosign: read data failed")
|
||||
// return
|
||||
// }
|
||||
|
||||
// fmt.Println("=========================== 3")
|
||||
// fmt.Println(hex.Dump(data))
|
||||
|
||||
// fmt.Println("=========================== 4")
|
||||
// ctx.Request.Body = &signReader{
|
||||
// *bytes.NewReader(data),
|
||||
// ctx.Request.Body,
|
||||
// }
|
||||
// fmt.Println("=========================== 5")
|
||||
// ctx.Next()
|
||||
// fmt.Println("=========================== 6")
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package api_security
|
||||
|
||||
const XProtoSign = "X-Proto-Sign"
|
||||
const XProtoNonce = "X-Proto-Nonce"
|
||||
const XProtoTimestamp = "X-Proto-Timestamp"
|
||||
@@ -0,0 +1,25 @@
|
||||
package api_security
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
func GenerateKey(secret string) []byte {
|
||||
key := sha256.Sum256([]byte(secret))
|
||||
return key[:]
|
||||
}
|
||||
|
||||
func GenerateIV(secret, salt string) []byte {
|
||||
return GenerateKey(secret + salt)[:16]
|
||||
}
|
||||
|
||||
const strPool = "012345679qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPLKJHGFDSAZXCVBNM"
|
||||
|
||||
func GenerateIVSalt() string {
|
||||
var n [16]byte
|
||||
for i := 0; i < 16; i++ {
|
||||
n[i] = byte(strPool[rand.Intn(len(strPool))])
|
||||
}
|
||||
return string(n[:])
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package api_security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateSalt(t *testing.T) {
|
||||
for i := 0; i < 5; i++ {
|
||||
fmt.Println(GenerateIVSalt())
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGenerateSalt(b *testing.B) {
|
||||
GenerateIV("asdfasdfasdf", GenerateIVSalt())
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package api_security
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type signReader struct {
|
||||
bytes.Reader
|
||||
RC io.ReadCloser
|
||||
}
|
||||
|
||||
func (thiz *signReader) Close() error {
|
||||
return thiz.RC.Close()
|
||||
}
|
||||
|
||||
func calcSign(kv map[string][]byte) string {
|
||||
keys := make([]string, 0, len(kv))
|
||||
for k := range kv {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
h := sha256.New()
|
||||
for _, k := range keys {
|
||||
_, _ = h.Write([]byte(k))
|
||||
_, _ = h.Write(kv[k])
|
||||
}
|
||||
|
||||
return strings.ToLower(hex.EncodeToString(h.Sum(nil)))
|
||||
}
|
||||
|
||||
func ProtoSign(secret []byte,
|
||||
shouldCrypto ShouldCryptoChecker) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
|
||||
if shouldCrypto != nil && !shouldCrypto(ctx) {
|
||||
ctx.Next()
|
||||
return
|
||||
}
|
||||
|
||||
sign := ctx.GetHeader(XProtoSign)
|
||||
nonce := ctx.GetHeader(XProtoNonce)
|
||||
ts := ctx.GetHeader(XProtoTimestamp)
|
||||
|
||||
if len(sign) != 64 || len(nonce) < 10 || len(ts) < 6 {
|
||||
ctx.AbortWithStatusJSON(500, "protosign: argument invalid")
|
||||
return
|
||||
}
|
||||
|
||||
data, e := ioutil.ReadAll(ctx.Request.Body)
|
||||
if e != nil {
|
||||
ctx.AbortWithStatusJSON(500, "protosign: read data failed")
|
||||
return
|
||||
}
|
||||
|
||||
kv := make(map[string][]byte)
|
||||
kv[XProtoNonce] = []byte(nonce)
|
||||
kv[XProtoTimestamp] = []byte(ts)
|
||||
kv["_METHOD_"] = []byte(ctx.Request.Method)
|
||||
kv["_PATH_"] = []byte(ctx.Request.URL.Path)
|
||||
kv["_SECRET_"] = secret
|
||||
|
||||
queries := map[string][]string(ctx.Request.URL.Query())
|
||||
for k, v := range queries {
|
||||
if len(v) > 0 {
|
||||
kv[fmt.Sprintf("_QUERY_%s_", strings.ToUpper(k))] = []byte(v[0])
|
||||
}
|
||||
}
|
||||
|
||||
if len(data) > 0 {
|
||||
kv["_BODY_"] = []byte(data)
|
||||
}
|
||||
|
||||
if calcSign(kv) != sign {
|
||||
ctx.AbortWithStatusJSON(500, "protosign: verify sign failed")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Request.Body = &signReader{
|
||||
*bytes.NewReader(data),
|
||||
ctx.Request.Body,
|
||||
}
|
||||
ctx.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package api_security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUrl(t *testing.T) {
|
||||
|
||||
u, _ := url.Parse("http://a.b.com/api/a/b/c?a=%20123")
|
||||
fmt.Printf("path: %s\n", u.Path)
|
||||
fmt.Printf("rawpath: %s\n", u.RawQuery)
|
||||
fmt.Printf("escapedpath: %s\n", u.EscapedPath())
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package api_security
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"io"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ShouldCryptoChecker func(ctx *gin.Context) bool
|
||||
|
||||
type cryptoReader struct {
|
||||
S cipher.Stream
|
||||
RC io.ReadCloser
|
||||
}
|
||||
|
||||
func (r *cryptoReader) Read(dst []byte) (n int, err error) {
|
||||
n, err = r.RC.Read(dst)
|
||||
r.S.XORKeyStream(dst[:n], dst[:n])
|
||||
return
|
||||
}
|
||||
|
||||
func (r *cryptoReader) Close() error {
|
||||
return r.RC.Close()
|
||||
}
|
||||
|
||||
type cryptoWriter struct {
|
||||
gin.ResponseWriter
|
||||
writer *cipher.StreamWriter
|
||||
}
|
||||
|
||||
func (g *cryptoWriter) WriteString(s string) (int, error) {
|
||||
return g.writer.Write([]byte(s))
|
||||
}
|
||||
|
||||
func (g *cryptoWriter) Write(data []byte) (int, error) {
|
||||
return g.writer.Write(data)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package apilimitHandler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type counter struct {
|
||||
Count int
|
||||
WindowStart time.Time
|
||||
}
|
||||
|
||||
type RouteHourlyLimiter struct {
|
||||
mu sync.Mutex
|
||||
limit int
|
||||
window time.Duration
|
||||
counters map[string]*counter
|
||||
}
|
||||
|
||||
func NewRouteHourlyLimiter(limit int) *RouteHourlyLimiter {
|
||||
l := &RouteHourlyLimiter{
|
||||
limit: limit,
|
||||
window: time.Hour,
|
||||
counters: make(map[string]*counter),
|
||||
}
|
||||
go l.cleaner()
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *RouteHourlyLimiter) Middleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 用 FullPath 做“接口维度”,例如 /api/user/:id
|
||||
route := c.FullPath()
|
||||
if route == "" {
|
||||
// 路由未匹配时兜底
|
||||
route = c.Request.URL.Path
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
item, ok := l.counters[route]
|
||||
if !ok || now.Sub(item.WindowStart) >= l.window {
|
||||
l.counters[route] = &counter{
|
||||
Count: 1,
|
||||
WindowStart: now,
|
||||
}
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if item.Count >= l.limit {
|
||||
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
|
||||
"code": http.StatusTooManyRequests,
|
||||
"message": "rate limit exceeded: max 100 requests per route per hour",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
item.Count++
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// 定时清理过期窗口,避免 map 无限增长
|
||||
func (l *RouteHourlyLimiter) cleaner() {
|
||||
ticker := time.NewTicker(10 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for now := range ticker.C {
|
||||
l.mu.Lock()
|
||||
for k, v := range l.counters {
|
||||
if now.Sub(v.WindowStart) >= l.window {
|
||||
delete(l.counters, k)
|
||||
}
|
||||
}
|
||||
l.mu.Unlock()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/errors"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/mwacc"
|
||||
"videoweb/pub/models/oplog"
|
||||
"videoweb/web/webg"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
var (
|
||||
RoleApiMenus = map[int64][]string{} // 角色Api
|
||||
mutex sync.RWMutex
|
||||
)
|
||||
|
||||
var (
|
||||
tokener = utils.NewTokenRedis()
|
||||
)
|
||||
|
||||
const (
|
||||
AccId = "ACC_ID"
|
||||
roleAuthAdrr = "/web/role/getauth"
|
||||
)
|
||||
|
||||
type APIInfo struct {
|
||||
Api string
|
||||
Desc string
|
||||
}
|
||||
|
||||
var (
|
||||
// 白名单,不需要验证token的api
|
||||
whiteList = []string{
|
||||
"/swagger",
|
||||
"/api/web/file/uploadImg",
|
||||
"/api/web/file/uploadImgs",
|
||||
"/api/web/media/videoBody",
|
||||
"/api/web/media/videoOk",
|
||||
"/api/web/m3u8",
|
||||
"/api/web/uploader/m3u8",
|
||||
"/api/web/enkey",
|
||||
"/api/web/login/acc",
|
||||
"/api/web/login/bind",
|
||||
"/api/web/login/slogin",
|
||||
"/api/web/stat/media/list",
|
||||
"/api/web/stat/analytics",
|
||||
"/api/web/stat/daily/analytics",
|
||||
"/api/web/sync/video/list",
|
||||
"/api/web/sync/videocollection/list",
|
||||
"/api/web/sync/category/list",
|
||||
"/api/web/sync/creator/list",
|
||||
"/api/web/sync/tag/list",
|
||||
"/api/web/contentexcel",
|
||||
"/api/web/videoinfo/fillZeroFakeFlow",
|
||||
"/wbapi",
|
||||
}
|
||||
)
|
||||
|
||||
// 读取角色信息
|
||||
func GetRoleApiMenus(ctx context.Context, accid int64) (roles []string, err error) {
|
||||
mutex.RLock()
|
||||
roles, ok := RoleApiMenus[accid]
|
||||
mutex.RUnlock()
|
||||
if !ok {
|
||||
accInfo, dataErr := mwacc.FindOneAccByFilter(nil, bson.M{mwacc.FN.MwAcc.Id: accid})
|
||||
if dataErr != nil || accInfo.Id == 0 {
|
||||
return roles, dataErr
|
||||
}
|
||||
|
||||
roleInfo, roleErr := mwacc.FindOneRoleByFilter(ctx, bson.M{mwacc.FN.MwRole.Id: accInfo.RoleId})
|
||||
|
||||
if roleErr != nil || roleInfo.Id == 0 {
|
||||
return roles, roleErr
|
||||
}
|
||||
|
||||
if roleInfo.Name == "超级管理员" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
RoleApiMenus[accid] = roleInfo.ApiMenus
|
||||
mutex.Unlock()
|
||||
roles = roleInfo.ApiMenus
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 是否角色api
|
||||
func HasRoleApi(path string, apis []string) bool {
|
||||
apimap := make(map[string]struct{})
|
||||
for _, v := range apis {
|
||||
apimap[v] = struct{}{}
|
||||
}
|
||||
_, ok := apimap[path]
|
||||
return ok
|
||||
}
|
||||
|
||||
// 角色权限
|
||||
func handleRoles(ctx *gin.Context) (int, error) {
|
||||
path := ctx.Request.URL.Path
|
||||
if strings.HasPrefix(path, roleAuthAdrr) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
accid := ctx.GetInt(AccId)
|
||||
apis, err := GetRoleApiMenus(ctx, int64(accid))
|
||||
if err != nil {
|
||||
return int(errors.RoleErr), err
|
||||
}
|
||||
if apis == nil {
|
||||
return 0, nil
|
||||
}
|
||||
has := HasRoleApi(path, apis)
|
||||
if !has {
|
||||
return int(errors.RoleErr), srvdef.New(srvdef.RoleErr, (srvdef.RoleErr).Msg(), "")
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// 楼风经纪人权限
|
||||
func handleLouFengBossRoles(ctx *gin.Context) (int, error) {
|
||||
path := ctx.Request.URL.Path
|
||||
if strings.HasPrefix(path, roleAuthAdrr) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
accid := ctx.GetInt64(AccId)
|
||||
accInfo, dataErr := mwacc.FindOneAccByFilter(nil, bson.M{mwacc.FN.MwAcc.Id: accid})
|
||||
if dataErr != nil || accInfo.Id == 0 {
|
||||
return int(errors.RoleErr), dataErr
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// 验证后台账号IP
|
||||
func authMwIP(ctx *gin.Context) (int, error) {
|
||||
|
||||
cIP := ctx.ClientIP()
|
||||
accid := ctx.GetInt64(AccId)
|
||||
accInfo, dataErr := mwacc.FindOneAccByFilter(nil, bson.M{mwacc.FN.MwAcc.Id: accid})
|
||||
if dataErr != nil || accInfo.Id == 0 {
|
||||
return int(errors.RoleErr), dataErr
|
||||
}
|
||||
if len(accInfo.LimitIP) != 0 {
|
||||
for _, v := range accInfo.LimitIP {
|
||||
if v == cIP {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
return int(errors.LimitIPErr), dataErr
|
||||
} else {
|
||||
return int(errors.LimitIPErr), dataErr
|
||||
}
|
||||
|
||||
// return 0, nil
|
||||
}
|
||||
|
||||
// 验证
|
||||
func Auth(ctx *gin.Context) {
|
||||
//for test
|
||||
//ctx.Set(AccId, int64(1000))
|
||||
//return
|
||||
p := ctx.Request.URL.Path
|
||||
if strings.Contains(p, "swagger") {
|
||||
return
|
||||
}
|
||||
for _, w := range whiteList {
|
||||
if strings.HasPrefix(ctx.Request.URL.Path, w) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if v, ok := common.ApiSetMaps[p]; ok {
|
||||
fmt.Println(v.IsWhite)
|
||||
if v.IsWhite {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
doToken := func() (int, error) {
|
||||
checkToken := ctx.Request.Header.Get("Authorization")
|
||||
// 去掉Bearer,如果有
|
||||
checkToken = utils.StripBearerPrefixFromTokenString(checkToken)
|
||||
|
||||
if checkToken == "" {
|
||||
return http.StatusUnauthorized, srvdef.New(srvdef.NoToken, (srvdef.NoToken).Msg(), "")
|
||||
}
|
||||
|
||||
has, err := mwacc.CheckToken(ctx, checkToken)
|
||||
if err != nil {
|
||||
return http.StatusUnauthorized, err
|
||||
}
|
||||
if !has {
|
||||
return http.StatusUnauthorized, srvdef.New(srvdef.ReLogin, (srvdef.ReLogin).Msg(), "")
|
||||
}
|
||||
|
||||
userId, _, err := utils.ParseUserToken(checkToken, webg.Conf.JwtKey)
|
||||
if err != nil {
|
||||
return http.StatusUnauthorized, srvdef.New(srvdef.InvalidToken, (srvdef.InvalidToken).Msg(), "")
|
||||
}
|
||||
// log.Info("Auth test", log.Any("checkToken", checkToken), log.Any("userId", userId))
|
||||
ctx.Set(AccId, userId)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
statusCode, err := doToken()
|
||||
if err != nil || statusCode != 0 {
|
||||
ctx.AbortWithStatusJSON(statusCode, err)
|
||||
return
|
||||
}
|
||||
|
||||
roleCode, err := handleRoles(ctx)
|
||||
if err != nil || roleCode != 0 {
|
||||
ctx.AbortWithStatusJSON(http.StatusForbidden, err)
|
||||
}
|
||||
|
||||
roleCode1, err1 := handleLouFengBossRoles(ctx)
|
||||
if err1 != nil || roleCode1 != 0 {
|
||||
ctx.AbortWithStatusJSON(http.StatusForbidden, err)
|
||||
}
|
||||
|
||||
// authIPCode, err2 := authMwIP(ctx)
|
||||
// if err2 != nil || authIPCode != 0 {
|
||||
// ctx.AbortWithStatusJSON(http.StatusForbidden, err)
|
||||
// }
|
||||
}
|
||||
|
||||
// func userAgentHandler(ctx *gin.Context) {
|
||||
// userAgent := ctx.Request.Header.Get("User-Agent")
|
||||
// if userAgent != "" {
|
||||
// fields := strings.Fields(userAgent)
|
||||
// curVersion := fields[len(fields)-1]
|
||||
// if strings.HasPrefix(curVersion, "version:") {
|
||||
// versionNum := strings.Split(curVersion, ":")[1]
|
||||
// ctx.Set("VERSION", versionNum)
|
||||
// } else {
|
||||
// ctx.Set("VERSION", "")
|
||||
// }
|
||||
// }
|
||||
// if strings.Contains(userAgent, "Android") {
|
||||
// ctx.Set("SYSTEM", "android")
|
||||
// }
|
||||
// if strings.Contains(userAgent, "iPhone") {
|
||||
// ctx.Set("SYSTEM", "ios")
|
||||
// }
|
||||
|
||||
// // 增加 不签名,不加密 开关
|
||||
// }
|
||||
|
||||
// 操作日志
|
||||
func OperateLog(ctx *gin.Context) {
|
||||
desc := ""
|
||||
find := false
|
||||
for _, w := range LogList {
|
||||
if ctx.Request.URL.Path == w.Api {
|
||||
find = true
|
||||
desc = w.Desc
|
||||
}
|
||||
}
|
||||
if !find {
|
||||
ctx.Next()
|
||||
return
|
||||
}
|
||||
|
||||
accId := ctx.GetInt64(AccId)
|
||||
rawData, err := ctx.GetRawData()
|
||||
ip := ctx.ClientIP()
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(400, err)
|
||||
}
|
||||
|
||||
log.Info("OperateLog Test", log.Any("vmap.Path", ctx.Request.URL.Path))
|
||||
|
||||
err = oplog.AddLog(ctx, ctx.Request.URL.Path, desc, accId, string(rawData), ip)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(int(errors.OperateLog), err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(rawData)) // 将取出来的执行条件放回ctx中
|
||||
ctx.Next()
|
||||
}
|
||||
|
||||
func GetAccount(ctx *gin.Context) int64 {
|
||||
return ctx.GetInt64(AccId)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package auth
|
||||
|
||||
var LogList = []APIInfo{
|
||||
{"/api/web/acc/add", "账号-添加"},
|
||||
{"/api/web/acc/del", "账号-删除"},
|
||||
{"/api/web/acc/up", "账号-更新"},
|
||||
{"/api/web/acc/savestatus", "账号-保存状态"},
|
||||
{"/api/web/acc/uppwd", "账号-更新密码"},
|
||||
{"/api/web/login/acc", "登录-登录"},
|
||||
{"/api/web/login/slogin", "管理员sso登陆"},
|
||||
{"/api/web/role/add", "角色-添加"},
|
||||
{"/api/web/role/del", "角色-删除"},
|
||||
{"/api/web/role/up", "角色-更新"},
|
||||
{"/api/web/role/enable", "角色-使能"},
|
||||
{"/api/web/role/upauth", "角色-更新权限"},
|
||||
{"/api/web/syscfg/update", "修改系统配置"},
|
||||
{"/api/web/syscfg/appkey", "设置媒资库appKey"},
|
||||
{"/api/web/post/comment/del", "删除一个评论"},
|
||||
{"/api/web/post/comment/user/del", "删除一个用户所有评论"},
|
||||
{"/api/web/post/comment/replay", "回复评论"},
|
||||
{"/api/web/post/comment/reviewBatch", "批量审核评论"},
|
||||
{"/api/web/post/comment/review", "审核评论"},
|
||||
{"/api/web/ipblack/add", "IP黑名单-增加"},
|
||||
{"/api/web/ipblack/del", "IP黑名单-删除"},
|
||||
{"/api/web/richtextcontent/add", "添加一个富文本内容"},
|
||||
{"/api/web/richtextcontent/del", "删除一个富文本内容"},
|
||||
{"/api/web/richtextcontent/update", "更新一个富文本内容"},
|
||||
{"/api/web/translationcategory/add", "添加一个导航分类"},
|
||||
{"/api/web/translationcategory/del", "删除一个导航分类"},
|
||||
{"/api/web/translationcategory/update", "更新一个导航分类"},
|
||||
{"/api/web/translation/add", "添加一个导航跳转信息"},
|
||||
{"/api/web/translation/del", "删除一个导航跳转信息"},
|
||||
{"/api/web/translation/update", "更新一个导航跳转信息"},
|
||||
{"/api/web/videocategory/add", "添加一个视频分类"},
|
||||
{"/api/web/videocategory/del", "删除一个视频分类"},
|
||||
{"/api/web/videocategory/update", "更新一个视频分类"},
|
||||
{"/api/web/videocreater/add", "添加一个视频作者"},
|
||||
{"/api/web/videocreater/del", "删除一个视频作者"},
|
||||
{"/api/web/videocreater/update", "更新一个视频作者"},
|
||||
{"/api/web/videotag/add", "添加一个标签管理"},
|
||||
{"/api/web/videotag/del", "删除一个标签管理"},
|
||||
{"/api/web/videotag/update", "更新一个标签管理"},
|
||||
{"/api/web/videoinfo/del", "删除一个视频管理"},
|
||||
{"/api/web/videoinfo/update", "更新一个视频管理"},
|
||||
{"/api/web/videoinfo/recommend/set", "视频-设置推荐"},
|
||||
{"/api/web/videoinfo/heat/set", "视频-设置热度"},
|
||||
{"/api/web/videocollection/media/option", "修改合集里的视频列表"},
|
||||
{"/api/web/videocollection/media/rank", "修改合集里的视频排序"},
|
||||
{"/api/web/videoinfo/addcoll", "新增一个视频合集"},
|
||||
{"/api/web/videoinfo/addTags", "视频-批量追加标签"},
|
||||
{"/api/web/videoinfo/fillZeroFakeFlow", "批量填充低播放量/低收藏视频假数据"},
|
||||
{"/api/web/actress/add", "添加一个女优信息"},
|
||||
{"/api/web/actress/del", "删除一个女优信息"},
|
||||
{"/api/web/actress/update", "更新一个女优信息"},
|
||||
{"/api/web/playlet/add", "短剧-新增"},
|
||||
{"/api/web/playlet/update", "短剧-编辑"},
|
||||
{"/api/web/playlet/del", "短剧-删除"},
|
||||
{"/api/web/playlet/status/set", "短剧-设置上下架"},
|
||||
{"/api/web/playlet/recommend/set", "短剧-设置推荐"},
|
||||
{"/api/web/playlet/featured/set", "短剧-设置爆剧"},
|
||||
{"/api/web/playlet/banner/set", "短剧-设置banner展示"},
|
||||
{"/api/web/playlet/theme/set", "短剧-设置题材"},
|
||||
{"/api/web/playlet/tag/set", "短剧-设置标签"},
|
||||
{"/api/web/playlet/like/virtual/set", "短剧-设置虚拟点赞数"},
|
||||
{"/api/web/playlet/browse/virtual/set", "短剧-设置虚拟观看数"},
|
||||
{"/api/web/playlet/heat/virtual/set", "短剧-设置虚拟热度"},
|
||||
{"/api/web/playlet/pay-type/set", "短剧-设置支付方式"},
|
||||
{"/api/web/playlet/watermark/reset", "短剧-重置水印"},
|
||||
{"/api/web/playlet/chapter/add", "短剧剧集-新增"},
|
||||
{"/api/web/playlet/chapter/update", "短剧剧集-更新"},
|
||||
{"/api/web/playlet/chapter/del", "短剧剧集-删除"},
|
||||
{"/api/web/playlet/chapter/pay-type/set", "短剧剧集-设置支付方式"},
|
||||
{"/api/web/playlet/chapter/sync", "短剧剧集-手动同步"},
|
||||
{"/api/web/playlet/category/set", "短剧-设置分类"},
|
||||
{"/api/web/playlet/category/add", "短剧分类-新增"},
|
||||
{"/api/web/playlet/category/update", "短剧分类-更新"},
|
||||
{"/api/web/playlet/category/del", "短剧分类-删除"},
|
||||
{"/api/web/playlet/home-config/add", "短剧首页配置-新增"},
|
||||
{"/api/web/playlet/home-config/update", "短剧首页配置-更新"},
|
||||
{"/api/web/playlet/home-config/del", "短剧首页配置-删除"},
|
||||
{"/api/web/playlet/theme/add", "短剧题材-新增"},
|
||||
{"/api/web/playlet/theme/update", "短剧题材-更新"},
|
||||
{"/api/web/playlet/theme/del", "短剧题材-删除"},
|
||||
{"/api/web/home/banner/add", "首页banner-新增"},
|
||||
{"/api/web/home/banner/update", "首页banner-更新"},
|
||||
{"/api/web/home/banner/del", "首页banner-删除"},
|
||||
{"/api/web/announcement/add", "公告-增加"},
|
||||
{"/api/web/announcement/del", "公告-删除"},
|
||||
{"/api/web/announcement/update", "公告-更新"},
|
||||
{"/api/web/content/tag/add", "统一标签-新增"},
|
||||
{"/api/web/content/tag/update", "统一标签-更新"},
|
||||
{"/api/web/content/tag/del", "统一标签-删除"},
|
||||
{"/api/web/slug/sync", "同步历史空slug"},
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package cors
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Origin *[]string // 传入空指针,表示允许"*", 传入空数组表示禁止跨域
|
||||
Methods []string // Methods
|
||||
AllowedHeaders []string // AllowHeaders
|
||||
ExposedHeaders []string // exposeHeader
|
||||
Credentials bool // cookie
|
||||
MaxAge int64 // 缓存MaxAge
|
||||
PreflightContinue bool // 在遇到Options的时候,继续,而不是返回请求
|
||||
OptionsSuccessStatus int // Options时,返回的状态码,默认204
|
||||
}
|
||||
|
||||
func configureOrigin(o *Options, c *gin.Context) map[string]string {
|
||||
reqOrigin := c.GetHeader("Origin")
|
||||
headers := make(map[string]string)
|
||||
if o.Origin == nil {
|
||||
headers["Access-Control-Allow-Origin"] = "*"
|
||||
} else {
|
||||
isAllowed := false
|
||||
for _, allowed := range *o.Origin {
|
||||
if allowed == reqOrigin {
|
||||
isAllowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isAllowed {
|
||||
headers["Access-Control-Allow-Origin"] = reqOrigin
|
||||
} else {
|
||||
headers["Access-Control-Allow-Origin"] = strconv.FormatBool(false)
|
||||
}
|
||||
headers["Vary"] = "Origin"
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
func configureMethods(o *Options) map[string]string {
|
||||
headers := make(map[string]string)
|
||||
if len(o.Methods) > 0 {
|
||||
headers["Access-Control-Allow-Methods"] = strings.ToUpper(strings.Join(o.Methods, ","))
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
func configureCredentials(o *Options) map[string]string {
|
||||
headers := make(map[string]string)
|
||||
if o.Credentials {
|
||||
headers["Access-Control-Allow-Credentials"] = strconv.FormatBool(true)
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
func configureAllowedHeaders(o *Options, c *gin.Context) map[string]string {
|
||||
headers := make(map[string]string)
|
||||
if len(o.AllowedHeaders) > 0 {
|
||||
headers["Access-Control-Allow-Headers"] = strings.Join(o.AllowedHeaders, ",")
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
func configureExposedHeaders(o *Options) map[string]string {
|
||||
var headers = make(map[string]string)
|
||||
if len(o.ExposedHeaders) > 0 {
|
||||
headers["Access-Control-Expose-Headers"] = strings.Join(o.ExposedHeaders, ",")
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
func configureMaxAge(o *Options) map[string]string {
|
||||
var headers = make(map[string]string)
|
||||
if o.MaxAge >= 0 {
|
||||
headers["Access-Control-Max-Age"] = strconv.FormatInt(o.MaxAge, 10)
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
func mergeMap(m ...map[string]string) map[string]string {
|
||||
r := make(map[string]string)
|
||||
for _, t := range m {
|
||||
for k, v := range t {
|
||||
r[k] = v
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func applyHeader(c *gin.Context, h map[string]string) {
|
||||
for k, v := range h {
|
||||
c.Header(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Cors 跨域处理
|
||||
func Cors(o *Options) gin.HandlerFunc {
|
||||
if o.OptionsSuccessStatus == 0 {
|
||||
o.OptionsSuccessStatus = http.StatusNoContent
|
||||
}
|
||||
return func(c *gin.Context) {
|
||||
method := c.Request.Method
|
||||
// c.Header("Access-Control-Allow-Origin", "*")
|
||||
// c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token,Authorization,Token,Content-Length,Etag,Content-Range,Accept-Ranges,User-Agent,Range,Bucket,Content-Disposition,Signature,X-Forwarded-For,X-Real-Ip")
|
||||
if method == "OPTIONS" {
|
||||
h := mergeMap(
|
||||
configureOrigin(o, c),
|
||||
configureCredentials(o),
|
||||
configureMethods(o),
|
||||
configureAllowedHeaders(o, c),
|
||||
configureMaxAge(o),
|
||||
configureExposedHeaders(o),
|
||||
)
|
||||
applyHeader(c, h)
|
||||
if o.PreflightContinue {
|
||||
c.Next()
|
||||
} else {
|
||||
c.Header("Content-Length", "0")
|
||||
c.AbortWithStatus(o.OptionsSuccessStatus)
|
||||
}
|
||||
} else {
|
||||
h := mergeMap(
|
||||
configureOrigin(o, c),
|
||||
configureCredentials(o),
|
||||
configureExposedHeaders(o),
|
||||
)
|
||||
applyHeader(c, h)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package ipblacklist
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"videoweb/web/service/srv_ipblack"
|
||||
)
|
||||
|
||||
func IpBlackListMiddleware(ctx *gin.Context) {
|
||||
ip := ctx.ClientIP()
|
||||
if srv_ipblack.CheckIp(ip) {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errors.New("login limit"))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package limitHandler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common"
|
||||
httpUtil "videoweb/pub/common/http"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/web/webg"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
var limitURI = []string{
|
||||
"/api/web/login/slogin",
|
||||
"/api/web/login/acc",
|
||||
}
|
||||
|
||||
var (
|
||||
tgBotToken = "2013237388:AAFwvKvEmQfEvcI7wt_64Zkp9KPfgHo8sow"
|
||||
tgGroupId = "-1003735177636"
|
||||
)
|
||||
|
||||
var (
|
||||
limitDuration = time.Minute
|
||||
limitCount = 5
|
||||
)
|
||||
|
||||
var memCache = cache.New(1*time.Minute, 10*time.Minute)
|
||||
|
||||
// LoginLimit 登陆频率限制:按账号维度限制,不依赖 ClientIP(可被伪造)。
|
||||
func LoginLimit(ctx *gin.Context) {
|
||||
path := ctx.Request.URL.Path
|
||||
|
||||
check := false
|
||||
for _, p := range limitURI {
|
||||
if strings.HasPrefix(path, p) {
|
||||
check = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !check {
|
||||
ctx.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 测试环境不限制
|
||||
if webg.Conf.RunMod != "prod" {
|
||||
ctx.Next()
|
||||
return
|
||||
}
|
||||
|
||||
var params map[string]interface{}
|
||||
var acc string
|
||||
if ctx.Request.Method == http.MethodPost {
|
||||
raw, _ := ctx.GetRawData()
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(raw))
|
||||
if err := json.Unmarshal(raw, ¶ms); err == nil {
|
||||
if a, ok := params["acc"].(string); ok {
|
||||
acc = strings.TrimSpace(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 无账号时无法按账号限流,交由后续登录逻辑处理参数错误
|
||||
if acc == "" {
|
||||
ctx.Next()
|
||||
return
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("web-limit:%s:%s", path, acc)
|
||||
|
||||
count := 1
|
||||
if aCount, ok := memCache.Get(key); ok {
|
||||
if n, ok2 := aCount.(int); ok2 {
|
||||
count = n + 1
|
||||
}
|
||||
}
|
||||
memCache.Set(key, count, limitDuration)
|
||||
|
||||
if count > limitCount {
|
||||
go func(acc, path string, count int, params map[string]interface{}) {
|
||||
message := fmt.Sprintf(
|
||||
"[91porn精选]\n登陆请求频率过高!\n时间: %s\n请求地址: %s\n账号: %s\n请求参数: %+v\n错误次数: %d",
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
path,
|
||||
acc,
|
||||
params,
|
||||
count,
|
||||
)
|
||||
// 请求地址/参数为不可信输入,去掉非法 UTF-8 字节;不用 parse_mode,避免 Markdown 把内容解析成实体后报错
|
||||
message = strings.ToValidUTF8(message, "")
|
||||
|
||||
url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", tgBotToken)
|
||||
req := map[string]interface{}{
|
||||
"chat_id": tgGroupId,
|
||||
"text": message,
|
||||
}
|
||||
jsonBytes, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
header := map[string]string{"Content-Type": "application/json"}
|
||||
resp, _ := httpUtil.Post(url, header, bytes.NewReader(jsonBytes))
|
||||
_ = resp.Data
|
||||
}(acc, path, count, params)
|
||||
|
||||
common.ServerJSON(ctx, srvdef.ErrReqForbidden, nil)
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Next()
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-playground/locales/en"
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
"gopkg.in/go-playground/validator.v9"
|
||||
en_translations "gopkg.in/go-playground/validator.v9/translations/en"
|
||||
)
|
||||
|
||||
var (
|
||||
ValidatorUni *ut.UniversalTranslator
|
||||
)
|
||||
|
||||
type defaultValidator struct {
|
||||
once sync.Once
|
||||
validate *validator.Validate
|
||||
}
|
||||
|
||||
var v binding.StructValidator = &defaultValidator{}
|
||||
|
||||
func init() {
|
||||
v.Engine()
|
||||
}
|
||||
|
||||
func (v *defaultValidator) ValidateStruct(obj interface{}) error {
|
||||
|
||||
if kindOfData(obj) == reflect.Struct {
|
||||
|
||||
v.lazyinit()
|
||||
|
||||
if err := v.validate.Struct(obj); err != nil {
|
||||
return error(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *defaultValidator) Engine() interface{} {
|
||||
v.lazyinit()
|
||||
return v.validate
|
||||
}
|
||||
|
||||
func (v *defaultValidator) lazyinit() {
|
||||
v.once.Do(func() {
|
||||
v.validate = validator.New()
|
||||
|
||||
// 依旧使用bingding tag兼容v8
|
||||
v.validate.SetTagName("binding")
|
||||
|
||||
// 错误信息转换
|
||||
en := en.New()
|
||||
ValidatorUni = ut.New(en, en)
|
||||
trans, _ := ValidatorUni.GetTranslator("en")
|
||||
en_translations.RegisterDefaultTranslations(v.validate, trans)
|
||||
|
||||
v.validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
|
||||
name := strings.SplitN(fld.Tag.Get("comment"), ",", 2)[0]
|
||||
if name == "-" {
|
||||
return ""
|
||||
}
|
||||
return name
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func kindOfData(data interface{}) reflect.Kind {
|
||||
|
||||
value := reflect.ValueOf(data)
|
||||
valueType := value.Kind()
|
||||
|
||||
if valueType == reflect.Ptr {
|
||||
valueType = value.Elem().Kind()
|
||||
}
|
||||
return valueType
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
)
|
||||
|
||||
// 替换gin 默认v8验证器
|
||||
// 改为v9
|
||||
func init() {
|
||||
binding.Validator = new(defaultValidator)
|
||||
}
|
||||
Reference in New Issue
Block a user