mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-06-16 22:54:12 +00:00
- Add `FileRouteReloader` to watch and atomically update routes from disk - Refactor `main` to support seamless configuration updates without restarts - Ensure existing routes are preserved if a reload encounters an error
106 lines
3.3 KiB
Go
106 lines
3.3 KiB
Go
package turn
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// turnCertReloadInterval is how often the TURNS certificate file is polled for
|
|
// changes. TLS cert renewals (Caddy DNS-01 for cdn.<base-domain>) happen on the
|
|
// order of weeks, so a minute of detection latency is irrelevant; polling keeps
|
|
// the dependency surface minimal (no fsnotify) and is robust across the
|
|
// atomic-rename pattern certbot/Caddy use when writing a renewed cert.
|
|
const turnCertReloadInterval = 60 * time.Second
|
|
|
|
// certReloader serves the current TURNS certificate through a tls.Config
|
|
// GetCertificate callback and hot-reloads it when the cert file changes on
|
|
// disk. This lets a Caddy-renewed certificate be picked up WITHOUT restarting
|
|
// the TURN server — a restart would tear down every active relay (~30s RTC
|
|
// drop for users mid-call). See plans/platform/04_STEALTH_TURN.md, the
|
|
// "cert renewal during cutover" note.
|
|
type certReloader struct {
|
|
certPath string
|
|
keyPath string
|
|
logger *zap.Logger
|
|
|
|
mu sync.RWMutex
|
|
cert *tls.Certificate
|
|
modTime time.Time
|
|
}
|
|
|
|
// newCertReloader loads the initial cert/key pair. Returns an error if the
|
|
// initial load fails — TURNS cannot start without a valid certificate.
|
|
func newCertReloader(certPath, keyPath string, logger *zap.Logger) (*certReloader, error) {
|
|
r := &certReloader{certPath: certPath, keyPath: keyPath, logger: logger}
|
|
if err := r.reload(); err != nil {
|
|
return nil, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// reload reads the cert/key pair from disk and atomically swaps it in. On
|
|
// failure it leaves the previously-loaded certificate in place: a renewal that
|
|
// momentarily presents a half-written or mismatched cert/key file must never
|
|
// take TURNS down — the old (still-valid) cert keeps serving until the next
|
|
// successful reload.
|
|
func (r *certReloader) reload() error {
|
|
cert, err := tls.LoadX509KeyPair(r.certPath, r.keyPath)
|
|
if err != nil {
|
|
return fmt.Errorf("load TURNS cert/key (%s): %w", r.certPath, err)
|
|
}
|
|
var mod time.Time
|
|
if fi, statErr := os.Stat(r.certPath); statErr == nil {
|
|
mod = fi.ModTime()
|
|
}
|
|
r.mu.Lock()
|
|
r.cert = &cert
|
|
r.modTime = mod
|
|
r.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// GetCertificate is the tls.Config.GetCertificate callback. It always returns
|
|
// the most recently loaded certificate, so every new TLS handshake uses the
|
|
// current cert without the listener being recreated.
|
|
func (r *certReloader) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
return r.cert, nil
|
|
}
|
|
|
|
// watch polls the cert file's mtime every interval and reloads when it advances.
|
|
// Blocks until stop is closed.
|
|
func (r *certReloader) watch(interval time.Duration, stop <-chan struct{}) {
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
case <-ticker.C:
|
|
fi, err := os.Stat(r.certPath)
|
|
if err != nil {
|
|
// File briefly absent during an atomic rename — retry next tick.
|
|
continue
|
|
}
|
|
r.mu.RLock()
|
|
unchanged := !fi.ModTime().After(r.modTime)
|
|
r.mu.RUnlock()
|
|
if unchanged {
|
|
continue
|
|
}
|
|
if err := r.reload(); err != nil {
|
|
r.logger.Warn("TURNS cert reload failed; keeping previous certificate",
|
|
zap.String("cert_path", r.certPath), zap.Error(err))
|
|
continue
|
|
}
|
|
r.logger.Info("TURNS cert hot-reloaded", zap.String("cert_path", r.certPath))
|
|
}
|
|
}
|
|
}
|