carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package relay
2
3import (
4 "bufio"
5 "context"
6 "encoding/base64"
7 "fmt"
8 "io"
9 "os"
10 "sort"
11 "strings"
12 "time"
13)
14
15// MasterKeyLen is the length of a region master in bytes (spec §4). Both
16// the parser and `carlos relay keygen` are held to it from here.
17const MasterKeyLen = 32
18
19type Master struct {
20 Kid string
21 Key []byte
22 RetireAt time.Time // zero means never
23}
24
25type Masters struct{ byKid map[string]Master }
26
27// ParseMasters reads "<kid> <base64 key> <retire_at|->" lines. Blank lines
28// and "#" comments are skipped. Keys must decode to exactly 32 bytes.
29func ParseMasters(r io.Reader) (*Masters, error) {
30 m := &Masters{byKid: map[string]Master{}}
31 sc := bufio.NewScanner(r)
32 line := 0
33 for sc.Scan() {
34 line++
35 s := strings.TrimSpace(sc.Text())
36 if s == "" || strings.HasPrefix(s, "#") {
37 continue
38 }
39 f := strings.Fields(s)
40 if len(f) != 3 {
41 return nil, fmt.Errorf("relay masters line %d: want 3 fields, got %d", line, len(f))
42 }
43 if !kidRe.MatchString(f[0]) {
44 return nil, fmt.Errorf("relay masters line %d: bad kid %q", line, f[0])
45 }
46 key, err := base64.StdEncoding.DecodeString(f[1])
47 if err != nil || len(key) != MasterKeyLen {
48 return nil, fmt.Errorf("relay masters line %d: key must be base64 of %d bytes", line, MasterKeyLen)
49 }
50 var retire time.Time
51 if f[2] != "-" {
52 retire, err = time.Parse(time.RFC3339, f[2])
53 if err != nil {
54 return nil, fmt.Errorf("relay masters line %d: bad retire_at %q", line, f[2])
55 }
56 }
57 m.byKid[f[0]] = Master{Kid: f[0], Key: key, RetireAt: retire}
58 }
59 return m, sc.Err()
60}
61
62func (m *Masters) Lookup(kid string, now time.Time) (Master, bool) {
63 mk, ok := m.byKid[kid]
64 if !ok || (!mk.RetireAt.IsZero() && !now.Before(mk.RetireAt)) {
65 return Master{}, false
66 }
67 return mk, true
68}
69
70// Retiring is the kids that carry a retire_at, and when it falls. An
71// operator mid-rotation reads it through `carlos relay status` to learn when
72// the old key may be dropped from /etc/carlos/relay-masters — the one fact
73// the CLI could not answer before (discussion #120). Kids with no retirement
74// date are absent rather than zero-valued, so "nothing is retiring" is an
75// empty map and not a map of zero times.
76func (m *Masters) Retiring() map[string]time.Time {
77 if m == nil {
78 return nil
79 }
80 out := map[string]time.Time{}
81 for k, mk := range m.byKid {
82 if !mk.RetireAt.IsZero() {
83 out[k] = mk.RetireAt
84 }
85 }
86 return out
87}
88
89func (m *Masters) Kids() []string {
90 out := make([]string, 0, len(m.byKid))
91 for k := range m.byKid {
92 out = append(out, k)
93 }
94 sort.Strings(out)
95 return out
96}
97
98// mastersPollInterval is how often WatchMasters checks path's mtime.
99// Overridden in tests to avoid a slow test suite.
100var mastersPollInterval = 5 * time.Second
101
102// WatchMasters re-reads path whenever its mtime changes, polling every
103// mastersPollInterval (fsnotify would be a dependency for one file).
104// onChange is called with the parse result or error each time the
105// content changes, and once at start.
106func WatchMasters(ctx context.Context, path string, onChange func(*Masters, error)) {
107 var last time.Time
108 tick := time.NewTicker(mastersPollInterval)
109 defer tick.Stop()
110 load := func() {
111 st, err := os.Stat(path)
112 if err != nil {
113 onChange(nil, err)
114 return
115 }
116 if st.ModTime().Equal(last) {
117 return
118 }
119 last = st.ModTime()
120 f, err := os.Open(path)
121 if err != nil {
122 onChange(nil, err)
123 return
124 }
125 defer f.Close()
126 onChange(ParseMasters(f))
127 }
128 load()
129 for {
130 select {
131 case <-ctx.Done():
132 return
133 case <-tick.C:
134 load()
135 }
136 }
137}
138