package sandbar import ( _ "embed" "log" "net/http" "sync" "time" ) //go:embed web/index.html var indexHTML []byte //go:embed web/status.html var statusHTML []byte type SMTPConfig struct{ Host, Port, User, Password, From string } // Options configures a Server. Zero values are sensible defaults. type Options struct { DataDir string URL string // public URL, used for links in notifications MasterKey string MetricsToken string SMTP SMTPConfig MessageDays int // how long topic messages are kept (default 30) CheckDays int // how long monitor results are kept (default 30) HitDays int // how long website analytics are kept (default 365) TrustProxy bool // take the client address from X-Forwarded-For, for use behind a reverse proxy // Self-update (see update.go). Version is the running release, such as v0.2.0; anything else // ("dev") never updates. Restart is called after a new binary is installed and should close // what the process holds open before re-executing it. Version string AutoUpdate bool UpdateServer string // default https://git.veillax.com UpdateRepo string // default Veillax/sandbar Restart func(exe string) error // Tests replace these. Now func() time.Time HTTPClient *http.Client Mailer func(to, subject, body string) error } type Server struct { opts Options st *Store hub *hub // monitor runtime state mu sync.Mutex running map[string]bool // monitor ids with a check in flight loginMu sync.Mutex loginTry map[string][]time.Time upd updateState anaMu sync.Mutex // website analytics: the visitor salt and a per-address rate limit anaSecret string anaTry map[string][]time.Time stop chan struct{} wg sync.WaitGroup } func New(opts Options, st *Store) *Server { if opts.MessageDays <= 0 { opts.MessageDays = 30 } if opts.CheckDays <= 0 { opts.CheckDays = 30 } if opts.HitDays <= 0 { opts.HitDays = 365 } return &Server{opts: opts, st: st, hub: newHub(), running: map[string]bool{}, loginTry: map[string][]time.Time{}, anaTry: map[string][]time.Time{}, stop: make(chan struct{})} } func (s *Server) now() time.Time { if s.opts.Now != nil { return s.opts.Now() } return time.Now() } // Start runs the background workers: monitor scheduler, escalation, pruning. func (s *Server) Start() { s.every(time.Second, func() { s.runDueMonitors(s.now()) }) s.every(5*time.Second, func() { s.escalate(s.now()) }) s.every(time.Hour, func() { s.st.prune(s.now(), s.opts.MessageDays, s.opts.CheckDays, s.opts.HitDays) }) } func (s *Server) Close() { close(s.stop) s.wg.Wait() } func (s *Server) every(d time.Duration, f func()) { s.wg.Add(1) go func() { defer s.wg.Done() t := time.NewTicker(d) defer t.Stop() for { select { case <-s.stop: return case <-t.C: func() { defer func() { if r := recover(); r != nil { log.Printf("worker panic: %v", r) } }() f() }() } } }() } func (s *Server) link(path string) string { if s.opts.URL == "" { return "" } return s.opts.URL + path } // BootstrapAdmin creates the first administrator from the environment when there are no users yet. func (s *Server) BootstrapAdmin(email, password string) { if email == "" || password == "" || s.countUsers() > 0 { return } if _, err := s.createUser("Admin", email, password, "admin", ""); err != nil { log.Printf("SANDBAR_ADMIN_EMAIL: %v", err) } }