package feedback import ( "context" "fmt" "io" "net/http" "os" "time" "rgdd.se/silentct/internal/logger" "rgdd.se/silentct/pkg/crtutil" "rgdd.se/silentct/pkg/policy" "rgdd.se/silentct/pkg/submission" ) type Event struct { NodeName string // Name of the node that generated a submission PEMChain []byte // A certificate chain found in the submission } type Config struct { Policy policy.Policy // Optional Logger *logger.Logger // Debug and info prints only (no output by default) PullInterval time.Duration // How often nodes are pulled via HTTP GET HTTPTimeout time.Duration // Timeout to use when pulling nodes } type Feedback struct { cfg Config nodes []policy.Node eventCh chan []Event } func New(cfg Config, eventCh chan []Event) (Feedback, error) { if !cfg.Logger.IsConfigured() { cfg.Logger = logger.New(logger.Config{Level: logger.LevelNotice, File: os.Stdout}) } if cfg.PullInterval == 0 { cfg.PullInterval = 1 * time.Hour } if cfg.HTTPTimeout == 0 { cfg.HTTPTimeout = 10 * time.Second } for i, node := range cfg.Policy.Nodes { if err := node.Validate(); err != nil { return Feedback{}, fmt.Errorf("node %d: %v", i, err) } } return Feedback{cfg: cfg, nodes: cfg.Policy.Nodes, eventCh: eventCh}, nil } // RunForever collects legitimately issued certificates from nodes func (fb *Feedback) RunForever(ctx context.Context) { ticker := time.NewTicker(fb.cfg.PullInterval) defer ticker.Stop() fb.pullOnce(ctx) for { select { case <-ticker.C: fb.pullOnce(ctx) case <-ctx.Done(): return } } } func (fb *Feedback) pullOnce(ctx context.Context) { fb.cfg.Logger.Debugf("pull %d nodes\n", len(fb.nodes)) for _, node := range fb.nodes { data, err := fb.pull(ctx, node) if err != nil { fb.cfg.Logger.Debugf("failed to pull node %s: %v", node.Name, err) continue } var events []Event for _, pemChain := range data { chain, err := crtutil.CertificateChainFromPEM(pemChain) if err != nil { fb.cfg.Logger.Infof("failed to parse certificate from node %s: %v", node.Name, err) continue } if err := node.Authorize(chain[0].DNSNames); err != nil { fb.cfg.Logger.Infof("%s\n", err.Error()) continue } events = append(events, Event{NodeName: node.Name, PEMChain: pemChain}) } fb.eventCh <- events } } func (fb *Feedback) pull(ctx context.Context, node policy.Node) ([][]byte, error) { req, err := http.NewRequest(http.MethodGet, node.URL, nil) if err != nil { return nil, fmt.Errorf("new request: %v", err) } req.WithContext(ctx) cli := http.Client{Timeout: fb.cfg.HTTPTimeout} rsp, err := cli.Do(req) if err != nil { return nil, fmt.Errorf("HTTP %s: %v", req.Method, err) } defer rsp.Body.Close() b, err := io.ReadAll(rsp.Body) if err != nil { return nil, fmt.Errorf("read response: %v", err) } if rsp.StatusCode != http.StatusOK { return nil, fmt.Errorf("%s: %s", rsp.Status, string(b)) } s := submission.Submission(b) data, err := s.Open(node) if err != nil { return nil, fmt.Errorf("open: %v", err) } return data, nil }