From 3694e24aaddb1ed5ac245a841f64bf50604acdb4 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 24 Jun 2025 12:39:51 +0800 Subject: [PATCH] Implement comment length and IP rate limiting in comment creation --- server/api/comment.go | 2 +- server/service/comment.go | 29 ++++++++++++++++++- server/service/image.go | 33 ++------------------- server/utils/request_limit.go | 54 +++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 33 deletions(-) create mode 100644 server/utils/request_limit.go diff --git a/server/api/comment.go b/server/api/comment.go index 428e310..bb57222 100644 --- a/server/api/comment.go +++ b/server/api/comment.go @@ -38,7 +38,7 @@ func createComment(c fiber.Ctx) error { return model.NewRequestError("Content cannot be empty") } - comment, err := service.CreateComment(req, userID, uint(resourceID)) + comment, err := service.CreateComment(req, userID, uint(resourceID), c.IP()) if err != nil { return err } diff --git a/server/service/comment.go b/server/service/comment.go index 84859b4..fdd1bf0 100644 --- a/server/service/comment.go +++ b/server/service/comment.go @@ -3,12 +3,20 @@ package service import ( "nysoure/server/dao" "nysoure/server/model" + "nysoure/server/utils" + "time" "github.com/gofiber/fiber/v3/log" ) const ( maxImagePerComment = 9 + maxCommentsPerIP = 512 // Maximum number of comments allowed per IP address per day + maxCommentLength = 1024 // Maximum length of a comment +) + +var ( + commentsLimiter = utils.NewRequestLimiter(maxCommentsPerIP, 24*time.Hour) ) type CommentRequest struct { @@ -16,7 +24,19 @@ type CommentRequest struct { Images []uint `json:"images"` } -func CreateComment(req CommentRequest, userID uint, resourceID uint) (*model.CommentView, error) { +func CreateComment(req CommentRequest, userID uint, resourceID uint, ip string) (*model.CommentView, error) { + if !commentsLimiter.AllowRequest(ip) { + log.Warnf("IP %s has exceeded the comment limit of %d comments per day", ip, maxCommentsPerIP) + return nil, model.NewRequestError("Too many comments from this IP address, please try again later") + } + + if len(req.Content) == 0 { + return nil, model.NewRequestError("Content cannot be empty") + } + if len([]rune(req.Content)) > maxCommentLength { + return nil, model.NewRequestError("Comment content exceeds maximum length of 1024 characters") + } + if len(req.Images) > maxImagePerComment { return nil, model.NewRequestError("Too many images, maximum is 9") } @@ -83,6 +103,13 @@ func ListCommentsWithUser(username string, page int) ([]model.CommentWithResourc } func UpdateComment(commentID, userID uint, req CommentRequest) (*model.CommentView, error) { + if len(req.Content) == 0 { + return nil, model.NewRequestError("Content cannot be empty") + } + if len([]rune(req.Content)) > maxCommentLength { + return nil, model.NewRequestError("Comment content exceeds maximum length of 1024 characters") + } + if len(req.Images) > maxImagePerComment { return nil, model.NewRequestError("Too many images, maximum is 9") } diff --git a/server/service/image.go b/server/service/image.go index 91e9f5c..99b7f77 100644 --- a/server/service/image.go +++ b/server/service/image.go @@ -11,7 +11,6 @@ import ( "nysoure/server/utils" "os" "strconv" - "sync" "time" "github.com/gofiber/fiber/v3/log" @@ -55,39 +54,11 @@ func init() { } var ( - imageUploadsByIP = make(map[string]uint) - imageUploadsLock = sync.RWMutex{} + imageLimiter = utils.NewRequestLimiter(maxUploadsPerIP, 24*time.Hour) ) const maxUploadsPerIP = 100 -func init() { - // Initialize the map with a cleanup function to remove old entries - go func() { - for { - time.Sleep(24 * time.Hour) // Cleanup every 24 hours - imageUploadsLock.Lock() - imageUploadsByIP = make(map[string]uint) // Clear the map - imageUploadsLock.Unlock() - } - }() -} - -func addIpUploadCount(ip string) bool { - imageUploadsLock.Lock() - defer imageUploadsLock.Unlock() - - count, exists := imageUploadsByIP[ip] - if !exists { - count = 0 - } - if count >= maxUploadsPerIP { - return false // Exceeded upload limit for this IP - } - imageUploadsByIP[ip] = count + 1 - return true // Upload count incremented successfully -} - func CreateImage(uid uint, ip string, data []byte) (uint, error) { canUpload, err := checkUserCanUpload(uid) if err != nil { @@ -96,7 +67,7 @@ func CreateImage(uid uint, ip string, data []byte) (uint, error) { } if !canUpload { // For a normal user, check the IP upload limit - if !addIpUploadCount(ip) { + if !imageLimiter.AllowRequest(ip) { return 0, model.NewUnAuthorizedError("You have reached the maximum upload limit") } } diff --git a/server/utils/request_limit.go b/server/utils/request_limit.go new file mode 100644 index 0000000..bbab0b0 --- /dev/null +++ b/server/utils/request_limit.go @@ -0,0 +1,54 @@ +package utils + +import ( + "sync" + "time" +) + +type RequestLimiter struct { + limit int + requestsByIP map[string]int + mu sync.Mutex +} + +func NewRequestLimiter(limit int, duration time.Duration) *RequestLimiter { + l := &RequestLimiter{ + limit: limit, + requestsByIP: make(map[string]int), + } + + if duration > 0 { + go func() { + for { + time.Sleep(duration) + l.resetCounts() + } + }() + } + + return l +} + +func (rl *RequestLimiter) AllowRequest(ip string) bool { + rl.mu.Lock() + defer rl.mu.Unlock() + + count, exists := rl.requestsByIP[ip] + if !exists { + count = 0 + } + + if count >= rl.limit { + return false // Exceeded request limit for this IP + } + + rl.requestsByIP[ip] = count + 1 + return true // Request allowed +} + +func (rl *RequestLimiter) resetCounts() { + rl.mu.Lock() + defer rl.mu.Unlock() + + rl.requestsByIP = make(map[string]int) // Reset all counts +}