Files
Wormhole/internal/auth/limiter.go
T
rose_cat707 0d84b07f68
CI / docker (push) Successful in 1m58s
feat: Wormhole 内网穿透与反向代理网关
Go + Vue 管理台:Agent 隧道、域名反代、IP 安全、访客验证与监控大盘。
Gitea Actions CI 多架构镜像;纯 Go SQLite、无 CGO Docker 构建。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 17:52:43 +08:00

124 lines
2.3 KiB
Go

package auth
import (
"sync"
"time"
"github.com/wormhole/wormhole/internal/config"
)
type LoginBlockedError struct {
RetryAfter time.Duration
}
func (e *LoginBlockedError) Error() string {
return "too many login attempts"
}
type loginRecord struct {
failures int
windowStart time.Time
lockedUntil time.Time
}
type LoginLimiter struct {
mu sync.Mutex
maxAttempts int
window time.Duration
lockout time.Duration
records map[string]*loginRecord
}
func NewLoginLimiter(cfg *config.AuthConfig) *LoginLimiter {
if cfg.LoginMaxAttempts <= 0 {
return nil
}
max := cfg.LoginMaxAttempts
window := cfg.LoginWindow
lockout := cfg.LoginLockout
if window <= 0 {
window = 5 * time.Minute
}
if lockout <= 0 {
lockout = 15 * time.Minute
}
return &LoginLimiter{
maxAttempts: max,
window: window,
lockout: lockout,
records: make(map[string]*loginRecord),
}
}
func (l *LoginLimiter) keys(ip, username string) []string {
keys := []string{"ip:" + ip}
if username != "" {
keys = append(keys, "user:"+username)
}
return keys
}
func (l *LoginLimiter) IsBlocked(ip, username string) (time.Duration, bool) {
if l == nil {
return 0, false
}
now := time.Now()
l.mu.Lock()
defer l.mu.Unlock()
var maxRetry time.Duration
for _, key := range l.keys(ip, username) {
rec := l.records[key]
if rec == nil {
continue
}
if now.Before(rec.lockedUntil) {
retry := time.Until(rec.lockedUntil)
if retry > maxRetry {
maxRetry = retry
}
}
}
return maxRetry, maxRetry > 0
}
func (l *LoginLimiter) RecordFailure(ip, username string) {
if l == nil {
return
}
now := time.Now()
l.mu.Lock()
defer l.mu.Unlock()
for _, key := range l.keys(ip, username) {
rec := l.records[key]
if rec == nil {
rec = &loginRecord{windowStart: now}
l.records[key] = rec
}
if now.Before(rec.lockedUntil) {
continue
}
if now.Sub(rec.windowStart) > l.window {
rec.failures = 0
rec.windowStart = now
}
rec.failures++
if rec.failures >= l.maxAttempts {
rec.lockedUntil = now.Add(l.lockout)
rec.failures = 0
}
}
}
func (l *LoginLimiter) Reset(ip, username string) {
if l == nil {
return
}
l.mu.Lock()
defer l.mu.Unlock()
for _, key := range l.keys(ip, username) {
delete(l.records, key)
}
}