// Package recon provides the OSINT/recon framework: per-source rate limiting, // stealth controls, robots.txt compliance, and parallel sweep orchestration. package recon import ( "context" "math/rand" "sync" "time" "golang.org/x/time/rate" ) // LimiterRegistry holds one *rate.Limiter per source name. // // RECON-INFRA-05: each source owns its own limiter — no centralization. // Lookups are idempotent: the same name always returns the same *rate.Limiter // pointer, guaranteeing that token state persists across calls for a source. type LimiterRegistry struct { mu sync.Mutex limiters map[string]*rate.Limiter } // NewLimiterRegistry constructs an empty registry. func NewLimiterRegistry() *LimiterRegistry { return &LimiterRegistry{limiters: make(map[string]*rate.Limiter)} } // For returns the limiter for name, creating it with (r, burst) on first call. // Repeat calls with the same name return the same *rate.Limiter pointer, // regardless of subsequent (r, burst) arguments — the first registration wins. func (lr *LimiterRegistry) For(name string, r rate.Limit, burst int) *rate.Limiter { lr.mu.Lock() defer lr.mu.Unlock() if l, ok := lr.limiters[name]; ok { return l } l := rate.NewLimiter(r, burst) lr.limiters[name] = l return l } // Wait blocks until the source's token is available. If stealth is true, // an additional random jitter between 100ms and 1s is applied after the token // acquisition to evade fingerprint-based detection (RECON-INFRA-06 partial — // fully wired with the stealth package in 09-03). // // Context cancellation is honored both during the underlying limiter.Wait and // during the jitter sleep; ctx.Err() is returned promptly in either case. func (lr *LimiterRegistry) Wait(ctx context.Context, name string, r rate.Limit, burst int, stealth bool) error { l := lr.For(name, r, burst) if err := l.Wait(ctx); err != nil { return err } if stealth { jitter := time.Duration(100+rand.Intn(900)) * time.Millisecond select { case <-time.After(jitter): case <-ctx.Done(): return ctx.Err() } } return nil }