server.go
| 1 | package server |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "net/http" |
| 6 | "path/filepath" |
| 7 | "runtime" |
| 8 | |
| 9 | "github.com/go-chi/chi/v5" |
| 10 | "github.com/go-chi/chi/v5/middleware" |
| 11 | |
| 12 | "vidarchive/internal/config" |
| 13 | "vidarchive/internal/handler" |
| 14 | "vidarchive/internal/service" |
| 15 | ) |
| 16 | |
| 17 | type Server struct { |
| 18 | router *chi.Mux |
| 19 | handler *handler.Handler |
| 20 | cfg *config.Config |
| 21 | } |
| 22 | |
| 23 | func New(cfg *config.Config, h *handler.Handler) *Server { |
| 24 | s := &Server{ |
| 25 | router: chi.NewRouter(), |
| 26 | handler: h, |
| 27 | cfg: cfg, |
| 28 | } |
| 29 | s.setupRoutes() |
| 30 | return s |
| 31 | } |
| 32 | |
| 33 | func (s *Server) setupRoutes() { |
| 34 | _, file, _, ok := runtime.Caller(0) |
| 35 | staticRoot := "web/static" |
| 36 | if ok { |
| 37 | root := filepath.Join(filepath.Dir(file), "..", "..") |
| 38 | staticRoot = filepath.Join(root, "web", "static") |
| 39 | } |
| 40 | |
| 41 | s.router.Use(middleware.Logger) |
| 42 | s.router.Use(middleware.Recoverer) |
| 43 | s.router.Use(s.securityHeaders) |
| 44 | s.router.Use(itemCacheMiddleware) |
| 45 | |
| 46 | s.router.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir(staticRoot)))) |
| 47 | |
| 48 | s.router.Get("/media/item/*", s.handler.ServeMediaItem) |
| 49 | |
| 50 | s.router.Get("/", func(w http.ResponseWriter, r *http.Request) { |
| 51 | http.Redirect(w, r, "/library", http.StatusSeeOther) |
| 52 | }) |
| 53 | s.router.Get("/library", s.handler.Library) |
| 54 | s.router.Get("/library/item/*", s.handler.LibraryItem) |
| 55 | s.router.Post("/library/item/*", s.handler.LibraryItem) |
| 56 | |
| 57 | s.router.Get("/queue", s.handler.Downloads) |
| 58 | s.router.Get("/queue/{id}", s.handler.DownloadDetail) |
| 59 | s.router.Post("/queue/{id}/delete", s.handler.DeleteDownload) |
| 60 | s.router.Post("/queue/clear", s.handler.ClearAllDownloads) |
| 61 | |
| 62 | s.router.Get("/download", s.handler.DownloadForm) |
| 63 | s.router.Post("/download", s.handler.CreateDownload) |
| 64 | |
| 65 | s.router.Get("/settings", s.handler.Settings) |
| 66 | s.router.Post("/settings/presets", s.handler.CreatePreset) |
| 67 | s.router.Post("/settings/presets/{id}", s.handler.UpdatePreset) |
| 68 | s.router.Post("/settings/presets/{id}/delete", s.handler.DeletePreset) |
| 69 | s.router.Post("/settings", s.handler.UpdateSettings) |
| 70 | |
| 71 | s.router.Post("/theme", s.handler.Theme) |
| 72 | |
| 73 | s.router.Get("/api/presets/{id}/flags", s.handler.GetPresetFlags) |
| 74 | } |
| 75 | |
| 76 | // itemCacheMiddleware attaches a fresh per-request library item cache so a |
| 77 | // single request that looks up the same item multiple times only scans it once. |
| 78 | func itemCacheMiddleware(next http.Handler) http.Handler { |
| 79 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 80 | next.ServeHTTP(w, r.WithContext(service.WithItemCache(r.Context()))) |
| 81 | }) |
| 82 | } |
| 83 | |
| 84 | func (s *Server) securityHeaders(next http.Handler) http.Handler { |
| 85 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 86 | w.Header().Set("X-Content-Type-Options", "nosniff") |
| 87 | w.Header().Set("X-Frame-Options", "DENY") |
| 88 | w.Header().Set("X-XSS-Protection", "1; mode=block") |
| 89 | w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") |
| 90 | |
| 91 | if s.cfg.IsHTTPS() { |
| 92 | w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") |
| 93 | w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; media-src 'self' blob:;") |
| 94 | } |
| 95 | |
| 96 | next.ServeHTTP(w, r) |
| 97 | }) |
| 98 | } |
| 99 | |
| 100 | func (s *Server) Router() http.Handler { |
| 101 | return s.router |
| 102 | } |
| 103 | |
| 104 | func (s *Server) Start() error { |
| 105 | addr := fmt.Sprintf(":%d", s.cfg.Port) |
| 106 | fmt.Printf("Starting server on %s\n", addr) |
| 107 | if s.cfg.BaseURL != "" { |
| 108 | fmt.Printf("Base URL: %s\n", s.cfg.BaseURL) |
| 109 | fmt.Printf("HTTPS mode: %v\n", s.cfg.IsHTTPS()) |
| 110 | } |
| 111 | return http.ListenAndServe(addr, s.router) |
| 112 | } |
| 113 |