package relay import ( "bufio" "context" "encoding/base64" "fmt" "io" "os" "sort" "strings" "time" ) // MasterKeyLen is the length of a region master in bytes (spec §4). Both // the parser and `carlos relay keygen` are held to it from here. const MasterKeyLen = 32 type Master struct { Kid string Key []byte RetireAt time.Time // zero means never } type Masters struct{ byKid map[string]Master } // ParseMasters reads " " lines. Blank lines // and "#" comments are skipped. Keys must decode to exactly 32 bytes. func ParseMasters(r io.Reader) (*Masters, error) { m := &Masters{byKid: map[string]Master{}} sc := bufio.NewScanner(r) line := 0 for sc.Scan() { line++ s := strings.TrimSpace(sc.Text()) if s == "" || strings.HasPrefix(s, "#") { continue } f := strings.Fields(s) if len(f) != 3 { return nil, fmt.Errorf("relay masters line %d: want 3 fields, got %d", line, len(f)) } if !kidRe.MatchString(f[0]) { return nil, fmt.Errorf("relay masters line %d: bad kid %q", line, f[0]) } key, err := base64.StdEncoding.DecodeString(f[1]) if err != nil || len(key) != MasterKeyLen { return nil, fmt.Errorf("relay masters line %d: key must be base64 of %d bytes", line, MasterKeyLen) } var retire time.Time if f[2] != "-" { retire, err = time.Parse(time.RFC3339, f[2]) if err != nil { return nil, fmt.Errorf("relay masters line %d: bad retire_at %q", line, f[2]) } } m.byKid[f[0]] = Master{Kid: f[0], Key: key, RetireAt: retire} } return m, sc.Err() } func (m *Masters) Lookup(kid string, now time.Time) (Master, bool) { mk, ok := m.byKid[kid] if !ok || (!mk.RetireAt.IsZero() && !now.Before(mk.RetireAt)) { return Master{}, false } return mk, true } // Retiring is the kids that carry a retire_at, and when it falls. An // operator mid-rotation reads it through `carlos relay status` to learn when // the old key may be dropped from /etc/carlos/relay-masters — the one fact // the CLI could not answer before (discussion #120). Kids with no retirement // date are absent rather than zero-valued, so "nothing is retiring" is an // empty map and not a map of zero times. func (m *Masters) Retiring() map[string]time.Time { if m == nil { return nil } out := map[string]time.Time{} for k, mk := range m.byKid { if !mk.RetireAt.IsZero() { out[k] = mk.RetireAt } } return out } func (m *Masters) Kids() []string { out := make([]string, 0, len(m.byKid)) for k := range m.byKid { out = append(out, k) } sort.Strings(out) return out } // mastersPollInterval is how often WatchMasters checks path's mtime. // Overridden in tests to avoid a slow test suite. var mastersPollInterval = 5 * time.Second // WatchMasters re-reads path whenever its mtime changes, polling every // mastersPollInterval (fsnotify would be a dependency for one file). // onChange is called with the parse result or error each time the // content changes, and once at start. func WatchMasters(ctx context.Context, path string, onChange func(*Masters, error)) { var last time.Time tick := time.NewTicker(mastersPollInterval) defer tick.Stop() load := func() { st, err := os.Stat(path) if err != nil { onChange(nil, err) return } if st.ModTime().Equal(last) { return } last = st.ModTime() f, err := os.Open(path) if err != nil { onChange(nil, err) return } defer f.Close() onChange(ParseMasters(f)) } load() for { select { case <-ctx.Done(): return case <-tick.C: load() } } }