From e18d36ebae30536c77c61cd5da123991e0ca1629 Mon Sep 17 00:00:00 2001 From: Rasmus Dahlberg Date: Sun, 31 Dec 2023 09:39:25 +0100 Subject: Add drafty prototype --- internal/feedback/feedback.go | 127 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 internal/feedback/feedback.go (limited to 'internal/feedback') diff --git a/internal/feedback/feedback.go b/internal/feedback/feedback.go new file mode 100644 index 0000000..77431e0 --- /dev/null +++ b/internal/feedback/feedback.go @@ -0,0 +1,127 @@ +package feedback + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "time" + + "rgdd.se/silent-ct/internal/logger" + "rgdd.se/silent-ct/pkg/crtutil" + "rgdd.se/silent-ct/pkg/policy" + "rgdd.se/silent-ct/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) + 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 +} -- cgit v1.2.3