package server import ( "fmt" "io/fs" "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "vidarchive" "vidarchive/internal/config" "vidarchive/internal/handler" ) type Server struct { router *chi.Mux handler *handler.Handler cfg *config.Config } func New(cfg *config.Config, h *handler.Handler) *Server { s := &Server{ router: chi.NewRouter(), handler: h, cfg: cfg, } s.setupRoutes() return s } func (s *Server) setupRoutes() { s.router.Use(middleware.Logger) s.router.Use(middleware.Recoverer) s.router.Use(s.securityHeaders) // Serve embedded static assets (fs.Sub strips the web/static prefix). The // error is only possible for an invalid constant path, so it can't occur here. staticFS, _ := fs.Sub(vidarchive.StaticFS, "web/static") s.router.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS)))) s.router.Get("/media/item/*", s.handler.ServeMediaItem) s.router.Get("/", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/library", http.StatusSeeOther) }) s.router.Get("/library", s.handler.Library) s.router.Get("/library/item/*", s.handler.LibraryItem) s.router.Post("/library/item/*", s.handler.LibraryItem) s.router.Get("/queue", s.handler.Downloads) s.router.Get("/queue/{id}", s.handler.DownloadDetail) s.router.Post("/queue/{id}/delete", s.handler.DeleteDownload) s.router.Post("/queue/clear", s.handler.ClearAllDownloads) s.router.Get("/download", s.handler.DownloadForm) s.router.Post("/download", s.handler.CreateDownload) s.router.Get("/subscriptions", s.handler.Subscriptions) s.router.Post("/subscriptions", s.handler.CreateSubscription) s.router.Post("/subscriptions/{id}", s.handler.UpdateSubscription) s.router.Post("/subscriptions/{id}/delete", s.handler.DeleteSubscription) s.router.Post("/subscriptions/{id}/toggle", s.handler.ToggleSubscription) s.router.Post("/subscriptions/{id}/run", s.handler.RunSubscription) s.router.Get("/settings", s.handler.Settings) s.router.Post("/settings/presets", s.handler.CreatePreset) s.router.Post("/settings/presets/{id}", s.handler.UpdatePreset) s.router.Post("/settings/presets/{id}/delete", s.handler.DeletePreset) s.router.Post("/settings", s.handler.UpdateSettings) s.router.Post("/theme", s.handler.Theme) s.router.Get("/api/presets/{id}/flags", s.handler.GetPresetFlags) } func (s *Server) securityHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("X-XSS-Protection", "1; mode=block") w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") if s.cfg.IsHTTPS() { w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; media-src 'self' blob:;") } next.ServeHTTP(w, r) }) } func (s *Server) Router() http.Handler { return s.router } func (s *Server) Start() error { addr := fmt.Sprintf(":%d", s.cfg.Port) fmt.Printf("Starting server on %s\n", addr) if s.cfg.BaseURL != "" { fmt.Printf("Base URL: %s\n", s.cfg.BaseURL) fmt.Printf("HTTPS mode: %v\n", s.cfg.IsHTTPS()) } return http.ListenAndServe(addr, s.router) }