110 lines
2.6 KiB
Go
110 lines
2.6 KiB
Go
// watcher.go — permanent whitelist file watcher
|
|
//
|
|
// The watcher polls the permanent whitelist file every cfg.WatchInterval
|
|
// seconds and reloads the in-memory store if the file has changed.
|
|
//
|
|
// Why poll instead of inotify?
|
|
//
|
|
// - Polling is simpler and works inside Docker containers where inotify
|
|
// may not be available (no host filesystem access).
|
|
// - The polling interval (30s default) is fast enough for admin
|
|
// whitelisting operations.
|
|
// - If you want inotify, add it later.
|
|
//
|
|
// We only reload when the file's mtime changes, so frequent touch-
|
|
// operations don't cause unnecessary reloads.
|
|
//
|
|
// The watcher uses a mutex to protect the in-memory store. This is
|
|
// necessary because the watcher runs in a separate goroutine and the
|
|
// auth handler reads from the same store.
|
|
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"log"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
// newPermanentWhitelistWatcher creates a watcher that reloads the
|
|
// permanent whitelist file from disk.
|
|
//
|
|
// The watcher loads the file once immediately on creation, then polls
|
|
// every interval.
|
|
func newPermanentWhitelistWatcher(path string, interval time.Duration, store *PermanentWhitelist) *watcher {
|
|
w := &watcher{
|
|
path: path,
|
|
interval: interval,
|
|
store: store,
|
|
}
|
|
w.reload()
|
|
return w
|
|
}
|
|
|
|
// watcher polls a file and reloads the whitelist when it changes.
|
|
type watcher struct {
|
|
path string
|
|
interval time.Duration
|
|
store *PermanentWhitelist
|
|
lastMod time.Time
|
|
}
|
|
|
|
// start begins the polling loop.
|
|
func (w *watcher) start() {
|
|
go func() {
|
|
ticker := time.NewTicker(w.interval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
w.reload()
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// reload reads the whitelist file and updates the in-memory store.
|
|
//
|
|
// We only reload if the file has changed (mtime). This prevents
|
|
// unnecessary reloads when the file is touched frequently.
|
|
func (w *watcher) reload() {
|
|
info, err := os.Stat(w.path)
|
|
if err != nil {
|
|
// File doesn't exist yet — skip.
|
|
return
|
|
}
|
|
|
|
// Only reload if the file has changed.
|
|
if info.ModTime().Equal(w.lastMod) {
|
|
return
|
|
}
|
|
w.lastMod = info.ModTime()
|
|
|
|
// Read and parse the file.
|
|
entries := make(map[string]struct{})
|
|
f, err := os.Open(w.path)
|
|
if err != nil {
|
|
log.Printf("failed to open whitelist file: %v", err)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if line == "" || line[0] == '#' {
|
|
continue
|
|
}
|
|
entries[line] = struct{}{}
|
|
}
|
|
|
|
// Update the store.
|
|
w.store.mu.Lock()
|
|
w.store.entries = entries
|
|
w.store.mu.Unlock()
|
|
|
|
log.Printf("loaded %d entries from %s", len(entries), w.path)
|
|
}
|