| new file mode 100644 |
| index 000000000..c0ec6ff52 |
| --- /dev/null |
| +++ b/internal/relay/masters.go |
| @@ -0,0 +1,110 @@ |
| +package relay |
| + |
| +import ( |
| + "bufio" |
| + "context" |
| + "encoding/base64" |
| + "fmt" |
| + "io" |
| + "os" |
| + "sort" |
| + "strings" |
| + "time" |
| +) |
| + |
| +type Master struct { |
| + Kid string |
| + Key []byte |
| + RetireAt time.Time // zero means never |
| +} |
| + |
| +type Masters struct{ byKid map[string]Master } |
| + |
| +// ParseMasters reads "<kid> <base64 key> <retire_at|->" 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) != 32 { |
| + return nil, fmt.Errorf("relay masters line %d: key must be base64 of 32 bytes", line) |
| + } |
| + 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 |
| +} |
| + |
| +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 |
| +} |
| + |
| +// WatchMasters re-reads path whenever its mtime changes, polling every |
| +// five seconds (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(5 * time.Second) |
| + 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() |
| + } |
| + } |
| +} |