mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-07 21:12:00 +08:00
Applied safe.GoNamed to: - 9 exchange order_sync goroutines (OKX, Hyperliquid, Aster, Bybit, KuCoin, Gate, Bitget, Lighter, Binance×2) - Drawdown monitor (auto_trader_risk.go) - Brain news scanner + market briefs (agent/brain.go) - Sentinel scanner (agent/sentinel.go) - Agent scheduler (agent/scheduler.go) - x402 idle watchdog (mcp/payment/x402.go) - MCP stream idle watchdog (mcp/client.go) - Rate limiter cleanup (api/rate_limiter.go) - 3 telemetry fire-and-forget sends (telemetry/experience.go) - CoinAnk WS handler (provider/coinank/coinank_api/kline_ws.go) - API server goroutine (main.go) Added manual defer/recover with error reporting to: - Telegram AI agent handler (sends error msg to user on panic) - Trader data fetch (returns error result on panic to prevent deadlock) Before: a panic in ANY of these 27 goroutines would crash the entire trading process with zero diagnostics. Now all panics are caught, logged with stack traces, and the process continues running.
134 lines
2.9 KiB
Go
134 lines
2.9 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"nofx/safe"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// loginAttempt tracks failed login attempts per IP
|
|
type loginAttempt struct {
|
|
count int
|
|
lastTry time.Time
|
|
blockedUntil time.Time
|
|
}
|
|
|
|
// LoginRateLimiter protects login endpoint from brute-force attacks
|
|
type LoginRateLimiter struct {
|
|
mu sync.Mutex
|
|
attempts map[string]*loginAttempt
|
|
|
|
maxAttempts int // max failures before blocking
|
|
blockTime time.Duration // how long to block after max failures
|
|
windowTime time.Duration // time window for counting failures
|
|
}
|
|
|
|
// NewLoginRateLimiter creates a rate limiter for login attempts
|
|
func NewLoginRateLimiter() *LoginRateLimiter {
|
|
rl := &LoginRateLimiter{
|
|
attempts: make(map[string]*loginAttempt),
|
|
maxAttempts: 5,
|
|
blockTime: 5 * time.Minute,
|
|
windowTime: 15 * time.Minute,
|
|
}
|
|
|
|
// Clean up stale entries every 10 minutes
|
|
safe.GoNamed("rate-limiter-cleanup", func() {
|
|
ticker := time.NewTicker(10 * time.Minute)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
rl.cleanup()
|
|
}
|
|
})
|
|
|
|
return rl
|
|
}
|
|
|
|
// Check returns true if the IP is allowed to attempt login
|
|
func (rl *LoginRateLimiter) Check(ip string) bool {
|
|
rl.mu.Lock()
|
|
defer rl.mu.Unlock()
|
|
|
|
a, exists := rl.attempts[ip]
|
|
if !exists {
|
|
return true
|
|
}
|
|
|
|
// Check if still blocked
|
|
if !a.blockedUntil.IsZero() && time.Now().Before(a.blockedUntil) {
|
|
return false
|
|
}
|
|
|
|
// Check if window expired — reset
|
|
if time.Since(a.lastTry) > rl.windowTime {
|
|
delete(rl.attempts, ip)
|
|
return true
|
|
}
|
|
|
|
return a.count < rl.maxAttempts
|
|
}
|
|
|
|
// RecordFailure records a failed login attempt
|
|
func (rl *LoginRateLimiter) RecordFailure(ip string) {
|
|
rl.mu.Lock()
|
|
defer rl.mu.Unlock()
|
|
|
|
a, exists := rl.attempts[ip]
|
|
if !exists {
|
|
a = &loginAttempt{}
|
|
rl.attempts[ip] = a
|
|
}
|
|
|
|
// Reset if window expired
|
|
if time.Since(a.lastTry) > rl.windowTime {
|
|
a.count = 0
|
|
a.blockedUntil = time.Time{}
|
|
}
|
|
|
|
a.count++
|
|
a.lastTry = time.Now()
|
|
|
|
if a.count >= rl.maxAttempts {
|
|
a.blockedUntil = time.Now().Add(rl.blockTime)
|
|
}
|
|
}
|
|
|
|
// RecordSuccess clears failed attempts for an IP after successful login
|
|
func (rl *LoginRateLimiter) RecordSuccess(ip string) {
|
|
rl.mu.Lock()
|
|
defer rl.mu.Unlock()
|
|
delete(rl.attempts, ip)
|
|
}
|
|
|
|
// cleanup removes stale entries
|
|
func (rl *LoginRateLimiter) cleanup() {
|
|
rl.mu.Lock()
|
|
defer rl.mu.Unlock()
|
|
|
|
now := time.Now()
|
|
for ip, a := range rl.attempts {
|
|
// Remove if both window and block have expired
|
|
if now.Sub(a.lastTry) > rl.windowTime && (a.blockedUntil.IsZero() || now.After(a.blockedUntil)) {
|
|
delete(rl.attempts, ip)
|
|
}
|
|
}
|
|
}
|
|
|
|
// LoginRateLimitMiddleware creates a gin middleware for login rate limiting
|
|
func LoginRateLimitMiddleware(rl *LoginRateLimiter) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
ip := c.ClientIP()
|
|
if !rl.Check(ip) {
|
|
c.JSON(http.StatusTooManyRequests, gin.H{
|
|
"error": "Too many login attempts. Please try again later.",
|
|
})
|
|
c.Abort()
|
|
return
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|