Files
Veillax 4cb24f7085
CI / test (push) Successful in 1m58s
Merge branch 'feature/mobile-push'
# Conflicts:
#	internal/sandbar/api.go
2026-10-27 17:14:00 -06:00

168 lines
4.6 KiB
Go

package sandbar
import (
"crypto/ecdsa"
_ "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
// Mobile companion (see push.go and mobile.go).
AppDir string // folder with the companion PWA, served at /app/
AppOrigin string // origin allowed to call the API from another site, such as https://sandbar-app.example.com
VapidSubject string // contact sent to push services: a mailto: or https: address (default: URL, if https)
PushAllowPrivate bool // allow push endpoints on private addresses and plain http, for development only
// Hammerhead link: when set, GET /api/v1/hammerhead/edges shows its edge and tunnel status.
HammerheadURL string
HammerheadEmail string
HammerheadPassword string
// 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
pushMu sync.Mutex // the VAPID key pair, loaded or created on first use
vapid *ecdsa.PrivateKey
vapidPub string
hhMu sync.Mutex
hhClient *http.Client
anaMu sync.Mutex // website analytics: the visitor salt and a per-address rate limit
anaSecret string
anaTry map[string][]time.Time
statusMu sync.Mutex // the status page settings cache and the signing secret
statusCfg *StatusSettings
statusKey []byte
subMu sync.Mutex // per-address limit on public subscribe requests
subTry map[string][]time.Time
notifyWG sync.WaitGroup // status page notifications in flight
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{}, subTry: 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(30*time.Second, func() { s.runMaintenance(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()
s.notifyWG.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)
}
}