aboutsummaryrefslogtreecommitdiff
path: root/pkg/storage/loglist/loglist.go
blob: ccc63b0fdf69f2c176b03549e7abe8a61425c7e9 (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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// Package loglist manages a list of logs to monitor.  The list of logs is based
// on Google's signed list.  Logs can also be added and removed manually.
package loglist

import (
	"context"
	"fmt"
	"time"

	"gitlab.torproject.org/rgdd/ct/pkg/metadata"
	"rgdd.se/silent-ct/internal/ioutil"
)

type Config struct {
	PermitBootstrap  bool   // Get and create initial log metadata if nothing valid is available on disk
	MetadataFile     string // Path to a dynamically updated metadata file that can be read/written
	HistoryDirectory string // Existing directory to store the history of downloaded metadata

	// Optional
	MetadataInFuture  time.Duration     // How wrong the metadata timestamp is allowed to be wrt. future dating
	MetadataGetsStale time.Duration     // How long until the metadata is considered stale
	MetadataIsRecent  time.Duration     // How long the metadata is considered recent
	HTTPTimeout       time.Duration     // Timeout when fetching metadata
	StaticLogs        []metadata.Log    // Takes precedence over the dynamically downloaded metadata
	RemoveLogs        []metadata.LogKey // Logs in the downloaded metadata with these keys are ignored
}

type LogList struct {
	cfg    Config
	md     metadata.Metadata
	source metadata.Loader
}

func New(cfg Config) (LogList, error) {
	if cfg.MetadataInFuture == 0 {
		cfg.MetadataInFuture = 24 * time.Hour
	}
	if cfg.MetadataGetsStale == 0 {
		cfg.MetadataGetsStale = 30 * 24 * time.Hour
	}
	if cfg.MetadataIsRecent == 0 {
		cfg.MetadataIsRecent = 1 * time.Hour
	}
	if cfg.HTTPTimeout == 0 {
		cfg.HTTPTimeout = 10 * time.Second
	}

	for i, log := range cfg.StaticLogs {
		if err := checkLog(log); err != nil {
			return LogList{}, fmt.Errorf("static logs: index %d: %v", i, err)
		}
	}
	for i, key := range cfg.RemoveLogs {
		if _, err := key.ID(); err != nil {
			return LogList{}, fmt.Errorf("remove logs: index %d: %v", i, err)
		}
	}

	s := metadata.NewHTTPSource(metadata.HTTPSourceOptions{Name: "google"})
	ll := LogList{cfg: cfg, source: &s}
	if err := ioutil.DirectoriesExist([]string{cfg.HistoryDirectory}); err != nil {
		return LogList{}, err
	}
	if err := ioutil.ReadJSON(cfg.MetadataFile, &ll.md); err != nil {
		if !ll.cfg.PermitBootstrap {
			return LogList{}, err
		}
		if _, _, err := ll.Update(context.Background()); err != nil {
			return LogList{}, err
		}
	}
	return ll, nil
}

func (ll *LogList) IsRecent() bool {
	return time.Now().Before(ll.md.CreatedAt.Add(ll.cfg.MetadataIsRecent))
}

func (ll *LogList) IsStale() bool {
	return time.Now().After(ll.md.CreatedAt.Add(ll.cfg.MetadataGetsStale))
}

func (ll *LogList) Generate() []metadata.Log {
	var configure []metadata.Log

	for _, log := range ll.cfg.StaticLogs {
		configure = append(configure, log)
	}
	for _, operator := range ll.md.Operators {
		for _, log := range operator.Logs {
			if findKey(ll.cfg.RemoveLogs, log) {
				continue // static configuration says to remove this log
			}
			if findLog(configure, log) {
				continue // static configuration takes precedence
			}
			if skipLog(log) {
				continue // not in a state where it makes sense to monitor
			}
			configure = append(configure, log)
		}
	}

	return configure
}

func (ll *LogList) Update(ctx context.Context) (added []metadata.Log, removed []metadata.Log, err error) {
	b, md, err := ll.archiveDynamic(ctx)
	if err != nil {
		return
	}
	if err = ioutil.CommitData(ll.cfg.MetadataFile, b); err != nil {
		return
	}

	added, removed = metadataLogDiff(ll.md, md)
	ll.md = md
	return
}

func (ll *LogList) archiveDynamic(ctx context.Context) ([]byte, metadata.Metadata, error) {
	llctx, cancel := context.WithTimeout(ctx, ll.cfg.HTTPTimeout)
	defer cancel()

	msg, sig, md, err := ll.source.Load(llctx)
	if err != nil {
		return nil, metadata.Metadata{}, err
	}
	if future := time.Now().Add(ll.cfg.MetadataInFuture); md.CreatedAt.After(future) {
		return nil, metadata.Metadata{}, fmt.Errorf("list created at %v is in the future", md.CreatedAt)
	}
	if md.CreatedAt.Before(ll.md.CreatedAt) {
		return nil, metadata.Metadata{}, fmt.Errorf("list created at %v is older than the current list", md.CreatedAt)
	}

	// FIXME: consider only archiving on major version bumps
	path := fmt.Sprintf("%s/%s.json", ll.cfg.HistoryDirectory, time.Now().Format("2006-01-02_1504"))
	if err := ioutil.CommitData(path, msg); err != nil {
		return nil, metadata.Metadata{}, err
	}
	if err := ioutil.CommitData(path+".sig", sig); err != nil {
		return nil, metadata.Metadata{}, err
	}
	return msg, md, nil
}