Compare commits

..

7 Commits

Author SHA1 Message Date
UUBulb
5a7e29f1bb
Merge 9598657a81 into 50ee62172f 2024-12-21 11:27:50 +00:00
uubulb
9598657a81 fix codeql 2024-12-21 19:27:36 +08:00
uubulb
f86b4f961b update waf api 2024-12-21 19:16:28 +08:00
uubulb
a839056d69 update user api error handling 2024-12-21 19:07:19 +08:00
uubulb
4f4c482103 update waf 2024-12-21 18:34:46 +08:00
naiba
50ee62172f feat: upgrade frontend
Some checks are pending
CodeQL / Analyze (go) (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
Contributors / contributors (push) Waiting to run
Sync / sync-to-jihulab (push) Waiting to run
2024-12-21 14:42:08 +08:00
naiba
f212144310 feat: add online user count
Some checks are pending
CodeQL / Analyze (go) (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
Contributors / contributors (push) Waiting to run
Sync / sync-to-jihulab (push) Waiting to run
Run Tests / tests (macos) (push) Waiting to run
Run Tests / tests (ubuntu) (push) Waiting to run
Run Tests / tests (windows) (push) Waiting to run
2024-12-21 10:59:05 +08:00
12 changed files with 154 additions and 60 deletions

View File

@ -88,18 +88,21 @@ func authenticator() func(c *gin.Context) (interface{}, error) {
} }
var user model.User var user model.User
realip := c.GetString(model.CtxKeyRealIPStr)
if err := singleton.DB.Select("id", "password").Where("username = ?", loginVals.Username).First(&user).Error; err != nil { if err := singleton.DB.Select("id", "password").Where("username = ?", loginVals.Username).First(&user).Error; err != nil {
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
model.BlockIP(singleton.DB, c.GetString(model.CtxKeyRealIPStr), model.WAFBlockReasonTypeLoginFail) model.BlockIP(singleton.DB, realip, model.WAFBlockReasonTypeLoginFail, model.BlockIDUnknownUser)
} }
return nil, jwt.ErrFailedAuthentication return nil, jwt.ErrFailedAuthentication
} }
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(loginVals.Password)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(loginVals.Password)); err != nil {
model.BlockIP(singleton.DB, c.GetString(model.CtxKeyRealIPStr), model.WAFBlockReasonTypeLoginFail) model.BlockIP(singleton.DB, realip, model.WAFBlockReasonTypeLoginFail, int64(user.ID))
return nil, jwt.ErrFailedAuthentication return nil, jwt.ErrFailedAuthentication
} }
model.ClearIP(singleton.DB, realip, model.BlockIDUnknownUser)
model.ClearIP(singleton.DB, realip, int64(user.ID))
return utils.Itoa(user.ID), nil return utils.Itoa(user.ID), nil
} }
} }
@ -169,10 +172,10 @@ func optionalAuthMiddleware(mw *jwt.GinJWTMiddleware) func(c *gin.Context) {
identity := mw.IdentityHandler(c) identity := mw.IdentityHandler(c)
if identity != nil { if identity != nil {
model.ClearIP(singleton.DB, c.GetString(model.CtxKeyRealIPStr)) model.ClearIP(singleton.DB, c.GetString(model.CtxKeyRealIPStr), model.BlockIDToken)
c.Set(mw.IdentityKey, identity) c.Set(mw.IdentityKey, identity)
} else { } else {
if err := model.BlockIP(singleton.DB, c.GetString(model.CtxKeyRealIPStr), model.WAFBlockReasonTypeBruteForceToken); err != nil { if err := model.BlockIP(singleton.DB, c.GetString(model.CtxKeyRealIPStr), model.WAFBlockReasonTypeBruteForceToken, model.BlockIDToken); err != nil {
waf.ShowBlockPage(c, err) waf.ShowBlockPage(c, err)
return return
} }

View File

@ -151,6 +151,6 @@ func batchDeleteUser(c *gin.Context) (any, error) {
return nil, singleton.Localizer.ErrorT("can't delete yourself") return nil, singleton.Localizer.ErrorT("can't delete yourself")
} }
singleton.OnUserDelete(ids) err := singleton.OnUserDelete(ids, newGormError)
return nil, singleton.DB.Where("id IN (?)", ids).Delete(&model.User{}).Error return nil, err
} }

View File

@ -1,6 +1,8 @@
package controller package controller
import ( import (
"strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/nezhahq/nezha/model" "github.com/nezhahq/nezha/model"
@ -13,12 +15,24 @@ import (
// @Schemes // @Schemes
// @Description List server // @Description List server
// @Tags auth required // @Tags auth required
// @Param limit query uint false "Page limit"
// @Param offset query uint false "Page offset"
// @Produce json // @Produce json
// @Success 200 {object} model.CommonResponse[[]model.WAFApiMock] // @Success 200 {object} model.CommonResponse[[]model.WAFApiMock]
// @Router /waf [get] // @Router /waf [get]
func listBlockedAddress(c *gin.Context) ([]*model.WAF, error) { func listBlockedAddress(c *gin.Context) ([]*model.WAF, error) {
limit, err := strconv.Atoi(c.Query("limit"))
if err != nil || limit < 1 {
limit = 25
}
offset, err := strconv.Atoi(c.Query("offset"))
if err != nil || offset < 1 {
offset = 1
}
var waf []*model.WAF var waf []*model.WAF
if err := singleton.DB.Find(&waf).Error; err != nil { if err := singleton.DB.Limit(limit).Offset(offset).Find(&waf).Error; err != nil {
return nil, err return nil, err
} }

View File

@ -107,6 +107,8 @@ func serverStream(c *gin.Context) (any, error) {
return nil, newWsError("%v", err) return nil, newWsError("%v", err)
} }
defer conn.Close() defer conn.Close()
singleton.OnlineUsers.Add(1)
defer singleton.OnlineUsers.Add(^uint64(0))
count := 0 count := 0
for { for {
stat, err := getServerStat(c, count == 0) stat, err := getServerStat(c, count == 0)
@ -164,6 +166,7 @@ func getServerStat(c *gin.Context, withPublicNote bool) ([]byte, error) {
return utils.Json.Marshal(model.StreamServerData{ return utils.Json.Marshal(model.StreamServerData{
Now: time.Now().Unix() * 1000, Now: time.Now().Unix() * 1000,
Online: singleton.OnlineUsers.Load(),
Servers: servers, Servers: servers,
}) })
}) })

View File

@ -0,0 +1,26 @@
package controller
import (
"sync/atomic"
"testing"
)
func TestWs(t *testing.T) {
onlineUsers := new(atomic.Uint64)
onlineUsers.Add(1)
if onlineUsers.Load() != 1 {
t.Error("onlineUsers.Add(1) failed")
}
onlineUsers.Add(1)
if onlineUsers.Load() != 2 {
t.Error("onlineUsers.Add(1) failed")
}
onlineUsers.Add(^uint64(0))
if onlineUsers.Load() != 1 {
t.Error("onlineUsers.Add(^uint64(0)) failed")
}
onlineUsers.Add(^uint64(0))
if onlineUsers.Load() != 0 {
t.Error("onlineUsers.Add(^uint64(0)) failed")
}
}

View File

@ -16,6 +16,7 @@ type StreamServer struct {
type StreamServerData struct { type StreamServerData struct {
Now int64 `json:"now,omitempty"` Now int64 `json:"now,omitempty"`
Online uint64 `json:"online,omitempty"`
Servers []StreamServer `json:"servers,omitempty"` Servers []StreamServer `json:"servers,omitempty"`
} }

View File

@ -19,6 +19,10 @@ type User struct {
} }
func (u *User) BeforeSave(tx *gorm.DB) error { func (u *User) BeforeSave(tx *gorm.DB) error {
if u.AgentSecret != "" {
return nil
}
key, err := utils.GenerateRandomString(32) key, err := utils.GenerateRandomString(32)
if err != nil { if err != nil {
return err return err

View File

@ -16,22 +16,30 @@ const (
WAFBlockReasonTypeAgentAuthFail WAFBlockReasonTypeAgentAuthFail
) )
const (
BlockIDgRPC = -127 + iota
BlockIDToken
BlockIDUnknownUser
)
type WAFApiMock struct { type WAFApiMock struct {
ID uint64 `json:"id,omitempty"`
IP string `json:"ip,omitempty"` IP string `json:"ip,omitempty"`
Count uint64 `json:"count,omitempty"` BlockReason uint8 `json:"block_reason,omitempty"`
LastBlockReason uint8 `json:"last_block_reason,omitempty"` BlockTimestamp uint64 `json:"block_timestamp,omitempty"`
LastBlockTimestamp uint64 `json:"last_block_timestamp,omitempty"` BlockIdentifier uint64 `json:"block_identifier,omitempty"`
} }
type WAF struct { type WAF struct {
IP []byte `gorm:"type:binary(16);primaryKey" json:"ip,omitempty"` ID uint64 `gorm:"primaryKey" json:"id,omitempty"`
Count uint64 `json:"count,omitempty"` IP []byte `gorm:"type:binary(16);index:idx_block_identifier" json:"ip,omitempty"`
LastBlockReason uint8 `json:"last_block_reason,omitempty"` BlockReason uint8 `json:"block_reason,omitempty"`
LastBlockTimestamp uint64 `json:"last_block_timestamp,omitempty"` BlockTimestamp uint64 `json:"block_timestamp,omitempty"`
BlockIdentifier int64 `gorm:"index:idx_block_identifier" json:"block_identifier,omitempty"`
} }
func (w *WAF) TableName() string { func (w *WAF) TableName() string {
return "waf" return "nz_waf"
} }
func CheckIP(db *gorm.DB, ip string) error { func CheckIP(db *gorm.DB, ip string) error {
@ -42,22 +50,31 @@ func CheckIP(db *gorm.DB, ip string) error {
if err != nil { if err != nil {
return err return err
} }
var w WAF
result := db.Limit(1).Find(&w, "ip = ?", ipBinary) var blockTimestamp uint64
result := db.Model(&WAF{}).Select("block_timestamp").Order("id desc").Where("ip = ?", ipBinary).Limit(1).Find(&blockTimestamp)
if result.Error != nil { if result.Error != nil {
return result.Error return result.Error
} }
if result.RowsAffected == 0 { // 检查是否未找到记录
// 检查是否未找到记录
if result.RowsAffected < 1 {
return nil return nil
} }
var count int64
if err := db.Model(&WAF{}).Where("ip = ?", ipBinary).Count(&count).Error; err != nil {
return err
}
now := time.Now().Unix() now := time.Now().Unix()
if powAdd(w.Count, 4, w.LastBlockTimestamp) > uint64(now) { if powAdd(uint64(count), 4, blockTimestamp) > uint64(now) {
return errors.New("you are blocked by nezha WAF") return errors.New("you are blocked by nezha WAF")
} }
return nil return nil
} }
func ClearIP(db *gorm.DB, ip string) error { func ClearIP(db *gorm.DB, ip string, uid int64) error {
if ip == "" { if ip == "" {
return nil return nil
} }
@ -65,7 +82,7 @@ func ClearIP(db *gorm.DB, ip string) error {
if err != nil { if err != nil {
return err return err
} }
return db.Unscoped().Delete(&WAF{}, "ip = ?", ipBinary).Error return db.Unscoped().Delete(&WAF{}, "ip = ? and block_identifier = ?", ipBinary, uid).Error
} }
func BatchClearIP(db *gorm.DB, ip []string) error { func BatchClearIP(db *gorm.DB, ip []string) error {
@ -83,7 +100,7 @@ func BatchClearIP(db *gorm.DB, ip []string) error {
return db.Unscoped().Delete(&WAF{}, "ip in (?)", ips).Error return db.Unscoped().Delete(&WAF{}, "ip in (?)", ips).Error
} }
func BlockIP(db *gorm.DB, ip string, reason uint8) error { func BlockIP(db *gorm.DB, ip string, reason uint8, uid int64) error {
if ip == "" { if ip == "" {
return nil return nil
} }
@ -91,16 +108,20 @@ func BlockIP(db *gorm.DB, ip string, reason uint8) error {
if err != nil { if err != nil {
return err return err
} }
var w WAF w := WAF{
w.IP = ipBinary IP: ipBinary,
BlockReason: reason,
BlockTimestamp: uint64(time.Now().Unix()),
BlockIdentifier: uid,
}
return db.Transaction(func(tx *gorm.DB) error { return db.Transaction(func(tx *gorm.DB) error {
if err := tx.Where(&w).Attrs(WAF{ var lastRecord WAF
LastBlockReason: reason, if err := tx.Model(&WAF{}).Order("id desc").Where("ip = ?", ipBinary).First(&lastRecord).Error; err != nil {
LastBlockTimestamp: uint64(time.Now().Unix()), if !errors.Is(err, gorm.ErrRecordNotFound) {
}).FirstOrCreate(&w).Error; err != nil {
return err return err
} }
return tx.Exec("UPDATE waf SET count = count + 1, last_block_reason = ?, last_block_timestamp = ? WHERE ip = ?", reason, uint64(time.Now().Unix()), ipBinary).Error }
return tx.Create(&w).Error
}) })
} }

View File

@ -41,12 +41,12 @@ func (a *authHandler) Check(ctx context.Context) (uint64, error) {
userId, ok := singleton.AgentSecretToUserId[clientSecret] userId, ok := singleton.AgentSecretToUserId[clientSecret]
if !ok && subtle.ConstantTimeCompare([]byte(clientSecret), []byte(singleton.Conf.AgentSecretKey)) != 1 { if !ok && subtle.ConstantTimeCompare([]byte(clientSecret), []byte(singleton.Conf.AgentSecretKey)) != 1 {
singleton.UserLock.RUnlock() singleton.UserLock.RUnlock()
model.BlockIP(singleton.DB, ip, model.WAFBlockReasonTypeAgentAuthFail) model.BlockIP(singleton.DB, ip, model.WAFBlockReasonTypeAgentAuthFail, model.BlockIDgRPC)
return 0, status.Error(codes.Unauthenticated, "客户端认证失败") return 0, status.Error(codes.Unauthenticated, "客户端认证失败")
} }
singleton.UserLock.RUnlock() singleton.UserLock.RUnlock()
model.ClearIP(singleton.DB, ip) model.ClearIP(singleton.DB, ip, model.BlockIDgRPC)
var clientUUID string var clientUUID string
if value, ok := md["client_uuid"]; ok { if value, ok := md["client_uuid"]; ok {

View File

@ -9,10 +9,10 @@
name: "Official" name: "Official"
repository: "https://github.com/hamster1963/nezha-dash-v1" repository: "https://github.com/hamster1963/nezha-dash-v1"
author: "hamster1963" author: "hamster1963"
version: "v1.7.4" version: "v1.7.6"
isofficial: true isofficial: true
- path: "nazhua-dist" - path: "nazhua-dist"
name: "Nazhua" name: "Nazhua"
repository: "https://github.com/hi2shark/nazhua" repository: "https://github.com/hi2shark/nazhua"
author: "hi2hi" author: "hi2hi"
version: "v0.4.22" version: "v0.4.23"

View File

@ -3,6 +3,7 @@ package singleton
import ( import (
_ "embed" _ "embed"
"log" "log"
"sync/atomic"
"time" "time"
"github.com/patrickmn/go-cache" "github.com/patrickmn/go-cache"
@ -23,6 +24,7 @@ var (
Loc *time.Location Loc *time.Location
FrontendTemplates []model.FrontendTemplate FrontendTemplates []model.FrontendTemplate
DashboardBootTime = uint64(time.Now().Unix()) DashboardBootTime = uint64(time.Now().Unix())
OnlineUsers = new(atomic.Uint64)
) )
//go:embed frontend-templates.yaml //go:embed frontend-templates.yaml

View File

@ -39,50 +39,65 @@ func OnUserUpdate(u *model.User) {
AgentSecretToUserId[u.AgentSecret] = u.ID AgentSecretToUserId[u.AgentSecret] = u.ID
} }
func OnUserDelete(id []uint64) { func OnUserDelete(id []uint64, errorFunc func(string, ...interface{}) error) error {
UserLock.Lock() UserLock.Lock()
defer UserLock.Unlock() defer UserLock.Unlock()
if len(id) < 1 { if len(id) < 1 {
return return Localizer.ErrorT("user id not specified")
} }
var ( var (
cron bool cron, server bool
server bool crons, servers []uint64
) )
for _, uid := range id { for _, uid := range id {
secret := UserIdToAgentSecret[uid] err := DB.Transaction(func(tx *gorm.DB) error {
delete(AgentSecretToUserId, secret)
delete(UserIdToAgentSecret, uid)
CronLock.RLock() CronLock.RLock()
crons := model.FindUserID(CronList, uid) crons = model.FindUserID(CronList, uid)
CronLock.RUnlock() CronLock.RUnlock()
cron = len(crons) > 0 cron = len(crons) > 0
if cron { if cron {
DB.Unscoped().Delete(&model.Cron{}, "id in (?)", crons) if err := tx.Unscoped().Delete(&model.Cron{}, "id in (?)", crons).Error; err != nil {
OnDeleteCron(crons) return err
}
} }
SortedServerLock.RLock() SortedServerLock.RLock()
servers := model.FindUserID(SortedServerList, uid) servers = model.FindUserID(SortedServerList, uid)
SortedServerLock.RUnlock() SortedServerLock.RUnlock()
server = len(servers) > 0 server = len(servers) > 0
if server { if server {
DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Unscoped().Delete(&model.Server{}, "id in (?)", servers).Error; err != nil { if err := tx.Unscoped().Delete(&model.Server{}, "id in (?)", servers).Error; err != nil {
return err return err
} }
if err := tx.Unscoped().Delete(&model.ServerGroupServer{}, "server_id in (?)", servers).Error; err != nil { if err := tx.Unscoped().Delete(&model.ServerGroupServer{}, "server_id in (?)", servers).Error; err != nil {
return err return err
} }
}
if err := tx.Unscoped().Delete(&model.Transfer{}, "server_id in (?)", servers).Error; err != nil {
return err
}
if err := tx.Where("id IN (?)", id).Delete(&model.User{}).Error; err != nil {
return err
}
return nil return nil
}) })
if err != nil {
return errorFunc("%v", err)
}
if cron {
OnDeleteCron(crons)
}
if server {
AlertsLock.Lock() AlertsLock.Lock()
for _, sid := range servers { for _, sid := range servers {
for _, alert := range Alerts { for _, alert := range Alerts {
@ -93,10 +108,13 @@ func OnUserDelete(id []uint64) {
} }
} }
} }
DB.Unscoped().Delete(&model.Transfer{}, "server_id in (?)", servers)
AlertsLock.Unlock() AlertsLock.Unlock()
OnServerDelete(servers) OnServerDelete(servers)
} }
secret := UserIdToAgentSecret[uid]
delete(AgentSecretToUserId, secret)
delete(UserIdToAgentSecret, uid)
} }
if cron { if cron {
@ -106,4 +124,6 @@ func OnUserDelete(id []uint64) {
if server { if server {
ReSortServer() ReSortServer()
} }
return nil
} }