Files
Wormhole/internal/auth/limiter.go
T
rose_cat707 612d68f56f feat: Wormhole 内网穿透与反向代理网关
Go + Vue 管理台,SQLite 持久化;支持隧道/域名穿透、域名转发插件链、访客认证、
IP 规则、API Key、Docker 单镜像部署;配置通过 Web 系统设置管理,无需 YAML 文件。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 12:15:02 +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)
}
}