server.go
| 1 | package server |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "net/http" |
| 6 | |
| 7 | "github.com/go-chi/chi/v5" |
| 8 | "github.com/go-chi/chi/v5/middleware" |
| 9 | |
| 10 | "vidarchive/internal/config" |
| 11 | "vidarchive/internal/handler" |
| 12 | ) |
| 13 | |
| 14 | type Server struct { |
| 15 | router *chi.Mux |
| 16 | handler *handler.Handler |
| 17 | cfg *config.Config |
| 18 | } |
| 19 | |
| 20 | func New(cfg *config.Config, h *handler.Handler) *Server { |
| 21 | s := &Server{ |
| 22 | router: chi.NewRouter(), |
| 23 | handler: h, |
| 24 | cfg: cfg, |
| 25 | } |
| 26 | s.setupRoutes() |
| 27 | return s |
| 28 | } |
| 29 | |
| 30 | func (s *Server) setupRoutes() { |
| 31 | // Middleware |
| 32 | s.router.Use(middleware.Logger) |
| 33 | s.router.Use(middleware.Recoverer) |
| 34 | s.router.Use(s.securityHeaders) |
| 35 | |
| 36 | // Static files |
| 37 | s.router.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static")))) |
| 38 | |
| 39 | // Media serving (with range request support via http.ServeFile) |
| 40 | s.router.Get("/media/{id}", s.handler.ServeMedia) |
| 41 | |
| 42 | // Pages |
| 43 | s.router.Get("/", func(w http.ResponseWriter, r *http.Request) { |
| 44 | http.Redirect(w, r, "/library", http.StatusSeeOther) |
| 45 | }) |
| 46 | s.router.Get("/library", s.handler.Library) |
| 47 | s.router.Get("/library/{id}", s.handler.LibraryDetail) |
| 48 | s.router.Post("/library/scan", s.handler.ScanLibrary) |
| 49 | s.router.Post("/library/{id}/delete", s.handler.DeleteMedia) |
| 50 | |
| 51 | s.router.Get("/queue", s.handler.Downloads) |
| 52 | s.router.Get("/queue/{id}", s.handler.DownloadDetail) |
| 53 | s.router.Post("/queue/{id}/delete", s.handler.DeleteDownload) |
| 54 | |
| 55 | s.router.Get("/download", s.handler.DownloadForm) |
| 56 | s.router.Post("/download", s.handler.CreateDownload) |
| 57 | |
| 58 | s.router.Get("/settings", s.handler.Settings) |
| 59 | s.router.Post("/settings/presets", s.handler.CreatePreset) |
| 60 | s.router.Post("/settings/presets/{id}", s.handler.UpdatePreset) |
| 61 | s.router.Post("/settings/presets/{id}/delete", s.handler.DeletePreset) |
| 62 | s.router.Post("/settings", s.handler.UpdateSettings) |
| 63 | |
| 64 | s.router.Post("/theme", s.handler.Theme) |
| 65 | |
| 66 | // API endpoints |
| 67 | s.router.Get("/api/presets/{id}/flags", s.handler.GetPresetFlags) |
| 68 | } |
| 69 | |
| 70 | func (s *Server) securityHeaders(next http.Handler) http.Handler { |
| 71 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 72 | w.Header().Set("X-Content-Type-Options", "nosniff") |
| 73 | w.Header().Set("X-Frame-Options", "DENY") |
| 74 | w.Header().Set("X-XSS-Protection", "1; mode=block") |
| 75 | w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") |
| 76 | |
| 77 | if s.cfg.IsHTTPS() { |
| 78 | w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") |
| 79 | w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; media-src 'self' blob:;") |
| 80 | } |
| 81 | |
| 82 | next.ServeHTTP(w, r) |
| 83 | }) |
| 84 | } |
| 85 | |
| 86 | func (s *Server) Start() error { |
| 87 | addr := fmt.Sprintf(":%d", s.cfg.Port) |
| 88 | fmt.Printf("Starting server on %s\n", addr) |
| 89 | if s.cfg.BaseURL != "" { |
| 90 | fmt.Printf("Base URL: %s\n", s.cfg.BaseURL) |
| 91 | fmt.Printf("HTTPS mode: %v\n", s.cfg.IsHTTPS()) |
| 92 | } |
| 93 | return http.ListenAndServe(addr, s.router) |
| 94 | } |
| 95 |