aboutsummaryrefslogtreecommitdiff
path: root/internal/feedback/feedback.go
blob: b191f050f38ab9c1e7c952e981cae64429748fa5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
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
}