fix confirmed deployment risks
This commit is contained in:
@@ -1,12 +1,23 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
restorePasswordMaxFailures = 5
|
||||
restorePasswordBlockWindow = 15 * time.Minute
|
||||
)
|
||||
|
||||
var backupRestoreAttemptLimiter = newRestoreAttemptLimiter(restorePasswordMaxFailures, restorePasswordBlockWindow)
|
||||
|
||||
type BackupHandler struct {
|
||||
backupService *service.BackupService
|
||||
userService *service.UserService
|
||||
@@ -165,6 +176,64 @@ type RestoreBackupRequest struct {
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type restoreAttemptState struct {
|
||||
failuresUntil time.Time
|
||||
failureCount int
|
||||
}
|
||||
|
||||
type restoreAttemptLimiter struct {
|
||||
mu sync.Mutex
|
||||
maxFailures int
|
||||
window time.Duration
|
||||
states map[int64]restoreAttemptState
|
||||
}
|
||||
|
||||
func newRestoreAttemptLimiter(maxFailures int, window time.Duration) *restoreAttemptLimiter {
|
||||
return &restoreAttemptLimiter{
|
||||
maxFailures: maxFailures,
|
||||
window: window,
|
||||
states: make(map[int64]restoreAttemptState),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *restoreAttemptLimiter) allow(userID int64, now time.Time) (bool, time.Duration) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
state, ok := l.states[userID]
|
||||
if !ok {
|
||||
return true, 0
|
||||
}
|
||||
if now.After(state.failuresUntil) {
|
||||
delete(l.states, userID)
|
||||
return true, 0
|
||||
}
|
||||
if state.failureCount < l.maxFailures {
|
||||
return true, 0
|
||||
}
|
||||
return false, state.failuresUntil.Sub(now)
|
||||
}
|
||||
|
||||
func (l *restoreAttemptLimiter) recordFailure(userID int64, now time.Time) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
state := l.states[userID]
|
||||
if now.After(state.failuresUntil) {
|
||||
state = restoreAttemptState{}
|
||||
}
|
||||
state.failureCount++
|
||||
state.failuresUntil = now.Add(l.window)
|
||||
l.states[userID] = state
|
||||
}
|
||||
|
||||
func (l *restoreAttemptLimiter) recordSuccess(userID int64) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
delete(l.states, userID)
|
||||
}
|
||||
|
||||
func (h *BackupHandler) RestoreBackup(c *gin.Context) {
|
||||
backupID := c.Param("id")
|
||||
if backupID == "" {
|
||||
@@ -185,6 +254,11 @@ func (h *BackupHandler) RestoreBackup(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if allowed, _ := backupRestoreAttemptLimiter.allow(sub.UserID, time.Now()); !allowed {
|
||||
response.ErrorFrom(c, infraerrors.TooManyRequests("RESTORE_PASSWORD_RATE_LIMITED", "too many failed password attempts, please try again later"))
|
||||
return
|
||||
}
|
||||
|
||||
// 获取管理员用户并验证密码
|
||||
user, err := h.userService.GetByID(c.Request.Context(), sub.UserID)
|
||||
if err != nil {
|
||||
@@ -192,9 +266,11 @@ func (h *BackupHandler) RestoreBackup(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if !user.CheckPassword(req.Password) {
|
||||
backupRestoreAttemptLimiter.recordFailure(sub.UserID, time.Now())
|
||||
response.BadRequest(c, "incorrect admin password")
|
||||
return
|
||||
}
|
||||
backupRestoreAttemptLimiter.recordSuccess(sub.UserID)
|
||||
|
||||
record, err := h.backupService.StartRestore(c.Request.Context(), backupID)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user