package checker import ( "context" "fmt" "log" "sync" "time" sdk "git.happydns.org/checker-sdk-go/checker" "git.happydns.org/checker-tls/contract" ) func (p *tlsProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (any, error) { raw, ok := sdk.GetOption[[]sdk.DiscoveryEntry](opts, OptionEndpoints) if !ok { return nil, fmt.Errorf("no discovery entries in options: did the host wire AutoFillDiscoveryEntries?") } timeoutMs := sdk.GetIntOption(opts, OptionProbeTimeoutMs, DefaultProbeTimeoutMs) if timeoutMs <= 0 { timeoutMs = DefaultProbeTimeoutMs } timeout := time.Duration(timeoutMs) * time.Millisecond enumerate := sdk.GetBoolOption(opts, OptionEnumerateCiphers, false) entries, warnings := contract.ParseEntries(raw) for _, w := range warnings { log.Printf("checker-tls: discarding malformed entry: %v", w) } // An empty entry set is not an error: it is the steady state on any // target where no producer has published yet, and the first run after // a fresh publication when the producer hasn't finished its own cycle. // The rule surfaces this as StatusUnknown rather than StatusError so a // freshly-enrolled domain doesn't flap red. if len(entries) == 0 { return &TLSData{Probes: map[string]TLSProbe{}, CollectedAt: time.Now()}, nil } probes := make(map[string]TLSProbe, len(entries)) var mu sync.Mutex var wg sync.WaitGroup sem := make(chan struct{}, MaxConcurrentProbes) dispatch: for _, e := range entries { select { case sem <- struct{}{}: case <-ctx.Done(): break dispatch } wg.Add(1) go func() { defer wg.Done() defer func() { <-sem }() pr := probe(ctx, e.Endpoint, timeout) log.Printf("checker-tls: %s %s:%d → tls=%s handshake_ok=%t elapsed=%dms err=%q", pr.Type, pr.Host, pr.Port, pr.TLSVersion, pr.TLSHandshakeOK, pr.ElapsedMS, pr.Error) if enumerate && pr.TLSHandshakeOK { enumRes, skipReason := enumerateEndpoint(ctx, e.Endpoint, enumerationBudget) switch { case enumRes != nil && enumRes.Skipped != "": pr.Enum = enumRes log.Printf("checker-tls: enum %s:%d → error: %s (duration=%dms)", pr.Host, pr.Port, enumRes.Skipped, enumRes.DurationMS) case enumRes != nil: pr.Enum = enumRes log.Printf("checker-tls: enum %s:%d → versions=%d duration=%dms", pr.Host, pr.Port, len(enumRes.Versions), enumRes.DurationMS) case skipReason != "": log.Printf("checker-tls: enum %s:%d → skipped: %s", pr.Host, pr.Port, skipReason) } } mu.Lock() probes[e.Ref] = pr mu.Unlock() }() } wg.Wait() return &TLSData{ Probes: probes, CollectedAt: time.Now(), }, nil }