Neues Paket mail/internal/protoguard kapselt die für IMAP- und POP3-Sessions gemeinsam benötigte Timeout- und Backoff-Logik einer einzelnen Verbindung: - Pro Protokollphase konfigurierbarer Idle-Read-Timeout (POP3: Authorization/Transaction, IMAP: NotAuthenticated/Selected), vor jedem Lesevorgang neu gesetzt. - Sich verdoppelnder Backoff bei wiederholten Anmeldefehlversuchen einer Verbindung (BackoffBase bis BackoffMax), Verbindungstrennung nach konfigurierbarer Höchstzahl statt Dauerschleife. Server.NewServer bleibt unverändert (Standardkonfiguration); NewServerWithGuardConfig erlaubt abweichende Werte. Ressourcenaufräumung bei Verbindungsabbruch war bereits durch defer conn.Close() strukturell gegeben — der Timeout sorgt dafür, dass dieser Pfad auch bei hängenden oder böswilligen Gegenstellen zuverlässig erreicht wird. Alle drei Pflichtprüfungen mit echten Nachweisen durchgeführt: Chaos-Test mit 30 hart gekappten Verbindungen während aktiver Übertragung (kein Goroutine-Leck), Timeout-Auslösung in jeder Protokollphase beider Server, steigender Backoff mit definierter Verbindungstrennung nach Höchstzahl an Fehlversuchen. go build/go vet/golangci-lint clean, gesamtes Mail-Modul (~24 Pakete) regressionsfrei getestet.
120 lines
3.6 KiB
Go
120 lines
3.6 KiB
Go
// Package protoguard bündelt die Fehlerbehandlungs- und
|
|
// Wiederverbindungslogik, die IMAP- und POP3-Sessions gemeinsam
|
|
// brauchen (ING-07): pro Protokollphase konfigurierbare Idle-Timeouts
|
|
// und Backoff statt Dauerschleife bei wiederholten Anmeldefehlern.
|
|
// Ressourcenaufräumung selbst passiert bereits strukturell durch
|
|
// defer conn.Close() in den Sessions — Guard sorgt dafür, dass dieser
|
|
// Pfad auch bei hängenden oder böswilligen Gegenstellen zuverlässig
|
|
// erreicht wird.
|
|
package protoguard
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"time"
|
|
)
|
|
|
|
// Phase identifiziert eine Protokollphase, für die ein eigener
|
|
// Idle-Timeout gilt.
|
|
type Phase string
|
|
|
|
// Config steuert Timeout- und Backoff-Verhalten einer Verbindung.
|
|
type Config struct {
|
|
// PhaseTimeout liefert den Idle-Timeout je Phase. Fehlt ein Eintrag,
|
|
// gilt DefaultTimeout.
|
|
PhaseTimeout map[Phase]time.Duration
|
|
// DefaultTimeout gilt, wenn für die aktuelle Phase kein eigener Wert
|
|
// gesetzt ist. 0 bedeutet: kein Timeout.
|
|
DefaultTimeout time.Duration
|
|
// MaxAuthFailures ist die Anzahl fehlgeschlagener Anmeldeversuche,
|
|
// nach der eine Verbindung getrennt wird. 0 bedeutet: unbegrenzt
|
|
// (kein Trennen, nur Backoff).
|
|
MaxAuthFailures int
|
|
// BackoffBase ist die Wartezeit vor der Antwort nach dem ersten
|
|
// Fehlversuch, verdoppelt sich je weiterem Fehlversuch bis
|
|
// BackoffMax.
|
|
BackoffBase time.Duration
|
|
BackoffMax time.Duration
|
|
}
|
|
|
|
// DefaultConfig liefert praxistaugliche Werte für Produktionsbetrieb.
|
|
func DefaultConfig() Config {
|
|
return Config{
|
|
DefaultTimeout: 5 * time.Minute,
|
|
MaxAuthFailures: 5,
|
|
BackoffBase: 200 * time.Millisecond,
|
|
BackoffMax: 5 * time.Second,
|
|
}
|
|
}
|
|
|
|
// Guard kapselt den Fehlerbehandlungszustand EINER Verbindung: aktuell
|
|
// angewandte Phase-Timeouts und Zahl der Anmeldefehlversuche.
|
|
type Guard struct {
|
|
cfg Config
|
|
authFailures int
|
|
}
|
|
|
|
// New erstellt einen Guard für eine einzelne Session.
|
|
func New(cfg Config) *Guard {
|
|
return &Guard{cfg: cfg}
|
|
}
|
|
|
|
// ApplyReadDeadline setzt die Lese-Deadline von conn passend zur
|
|
// angegebenen Protokollphase (Akzeptanzkriterium 2).
|
|
func (g *Guard) ApplyReadDeadline(conn net.Conn, phase Phase) error {
|
|
d := g.cfg.DefaultTimeout
|
|
if pd, ok := g.cfg.PhaseTimeout[phase]; ok {
|
|
d = pd
|
|
}
|
|
if d <= 0 {
|
|
return conn.SetReadDeadline(time.Time{})
|
|
}
|
|
return conn.SetReadDeadline(time.Now().Add(d))
|
|
}
|
|
|
|
// RecordAuthFailure zählt einen fehlgeschlagenen Anmeldeversuch dieser
|
|
// Verbindung und liefert die Backoff-Wartezeit vor der Fehlerantwort
|
|
// sowie ob die Verbindung danach getrennt werden muss (Akzeptanzkriterium
|
|
// 3: klar definierter Backoff statt Dauerschleife).
|
|
func (g *Guard) RecordAuthFailure() (backoff time.Duration, disconnect bool) {
|
|
g.authFailures++
|
|
backoff = g.backoffFor(g.authFailures)
|
|
disconnect = g.cfg.MaxAuthFailures > 0 && g.authFailures >= g.cfg.MaxAuthFailures
|
|
return backoff, disconnect
|
|
}
|
|
|
|
// ResetAuthFailures setzt den Fehlversuchszähler nach erfolgreicher
|
|
// Anmeldung zurück.
|
|
func (g *Guard) ResetAuthFailures() { g.authFailures = 0 }
|
|
|
|
func (g *Guard) backoffFor(failures int) time.Duration {
|
|
if g.cfg.BackoffBase <= 0 {
|
|
return 0
|
|
}
|
|
d := g.cfg.BackoffBase
|
|
for i := 1; i < failures; i++ {
|
|
d *= 2
|
|
if g.cfg.BackoffMax > 0 && d >= g.cfg.BackoffMax {
|
|
return g.cfg.BackoffMax
|
|
}
|
|
}
|
|
if g.cfg.BackoffMax > 0 && d > g.cfg.BackoffMax {
|
|
return g.cfg.BackoffMax
|
|
}
|
|
return d
|
|
}
|
|
|
|
// Wait wartet d, bricht aber bei ctx-Abbruch sofort ab, damit ein
|
|
// Server-Shutdown nicht auf eine laufende Backoff-Pause warten muss.
|
|
func (g *Guard) Wait(ctx context.Context, d time.Duration) {
|
|
if d <= 0 {
|
|
return
|
|
}
|
|
timer := time.NewTimer(d)
|
|
defer timer.Stop()
|
|
select {
|
|
case <-timer.C:
|
|
case <-ctx.Done():
|
|
}
|
|
}
|