prism/service/server/server.go

174 lines
4.4 KiB
Go

package server
import (
"context"
"fmt"
"log/slog"
"net/http"
"time"
"prism/service/config"
"prism/service/notification"
"prism/service/proton"
"prism/service/signal"
"prism/service/util"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
type Server struct {
cfg *config.Config
store *notification.Store
dispatcher *notification.Dispatcher
protonMonitor *proton.Monitor
signalDaemon *signal.Daemon
linkDevice *signal.LinkDevice
logger *slog.Logger
router *chi.Mux
httpServer *http.Server
startTime time.Time
}
func New(cfg *config.Config) (*Server, error) {
logger := util.NewLogger(cfg.VerboseLogging)
store, err := notification.NewStore(cfg.StoragePath)
if err != nil {
return nil, fmt.Errorf("failed to create store: %w", err)
}
signalClient := signal.NewClient(cfg.SignalCLISocketPath)
dispatcher := notification.NewDispatcher(store, signalClient, logger)
protonMonitor := proton.NewMonitor(cfg, dispatcher, logger)
signalDaemon := signal.NewDaemon(cfg.SignalCLIBinaryPath, cfg.SignalCLIDataPath, cfg.SignalCLISocketPath)
linkDevice := signal.NewLinkDevice(signalClient, cfg.DeviceName)
s := &Server{
cfg: cfg,
store: store,
dispatcher: dispatcher,
protonMonitor: protonMonitor,
signalDaemon: signalDaemon,
linkDevice: linkDevice,
logger: logger,
startTime: time.Now(),
}
s.setupRoutes()
return s, nil
}
func (s *Server) setupRoutes() {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Recoverer)
r.Use(loggingMiddleware(s.logger))
r.Use(securityHeadersMiddleware())
r.Use(middleware.Timeout(5 * time.Second))
r.Use(middleware.Compress(5))
r.Use(middleware.StripSlashes)
r.Use(rateLimitMiddleware(s.cfg.RateLimit))
r.Handle("/*", http.StripPrefix("/", http.FileServer(http.Dir("./public"))))
r.Route("/fragment", func(r chi.Router) {
r.Use(authMiddleware(s.cfg.APIKey))
r.Get("/health", s.handleFragmentHealth)
r.Get("/signal-info", s.handleFragmentSignalInfo)
r.Get("/endpoints", s.handleFragmentEndpoints)
})
r.Route("/action", func(r chi.Router) {
r.Use(authMiddleware(s.cfg.APIKey))
r.Delete("/delete-endpoint", s.handleDeleteEndpointAction)
r.Post("/toggle-channel", s.handleToggleChannelAction)
})
r.Route("/admin", func(r chi.Router) {
r.Use(authMiddleware(s.cfg.APIKey))
r.Get("/mappings", s.handleGetMappings)
r.Post("/mappings", s.handleCreateMapping)
r.Delete("/mappings/{endpoint}", s.handleDeleteMapping)
r.Put("/mappings/{endpoint}/channel", s.handleUpdateChannel)
r.Get("/stats", s.handleGetStats)
})
r.Route("/api/webhook", func(r chi.Router) {
r.Use(authMiddleware(s.cfg.APIKey))
r.Post("/register", s.handleWebhookRegister)
})
r.Route("/api/proton-mail", func(r chi.Router) {
r.Use(authMiddleware(s.cfg.APIKey))
r.Post("/mark-read", s.handleProtonMarkRead)
})
r.With(authMiddleware(s.cfg.APIKey)).Post("/{topic}", s.handleNtfyPublish)
s.router = r
}
func (s *Server) Start(ctx context.Context) error {
s.logger.Info("Starting Signal daemon")
if err := s.signalDaemon.Start(); err != nil {
return fmt.Errorf("failed to start signal daemon: %w", err)
}
go func() {
if err := s.protonMonitor.Start(ctx); err != nil && err != context.Canceled {
s.logger.Error("Proton monitor error", "error", err)
}
}()
addr := fmt.Sprintf(":%d", s.cfg.Port)
s.httpServer = &http.Server{
Addr: addr,
Handler: s.router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
s.logger.Info("Prism running on:")
s.logger.Info(fmt.Sprintf("Local: http://localhost:%d", s.cfg.Port))
if lanIP := util.GetLANIP(); lanIP != "" {
s.logger.Debug(fmt.Sprintf("Network: http://%s:%d", lanIP, s.cfg.Port))
}
errCh := make(chan error, 1)
go func() {
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errCh <- err
}
}()
select {
case <-ctx.Done():
return s.Shutdown()
case err := <-errCh:
return err
}
}
func (s *Server) Shutdown() error {
s.logger.Info("Shutting down server")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := s.httpServer.Shutdown(ctx); err != nil {
return fmt.Errorf("failed to shutdown http server: %w", err)
}
if err := s.store.Close(); err != nil {
return fmt.Errorf("failed to close store: %w", err)
}
return nil
}