aboutsummaryrefslogtreecommitdiff
path: root/internal/feedback/feedback.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/feedback/feedback.go')
-rw-r--r--internal/feedback/feedback.go127
1 files changed, 127 insertions, 0 deletions
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
+}