package staticimages import ( "io" "io/fs" "net/http" "os" "path" "strings" "time" "github.com/go-chi/chi/v5" ) const cacheControl = "public, max-age=31536000, immutable" const svgContentSecurityPolicy = "default-src 'none'; style-src 'unsafe-inline'; sandbox" const fallbackContentType = "application/octet-stream" var contentTypeExtensions = map[string]string{ ".png": "image/png", ".jpeg": "image/jpeg", ".webp": "image/webp", ".svg": "image/svg+xml", } type Handler struct { uploadDir fs.FS modTime time.Time } func NewHandler(uploadDir string) *Handler { return &Handler{ uploadDir: os.DirFS(uploadDir), modTime: time.Now(), } } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff") cleaned := cleanPath(requestedPath(r)) file, stat, ok := h.openFile(cleaned) if !ok { http.NotFound(w, r) return } defer file.Close() content, ok := file.(io.ReadSeeker) if !ok { http.Error(w, "File not seekable", http.StatusInternalServerError) return } contentType := contentTypeFor(cleaned) w.Header().Set("Content-Type", contentType) w.Header().Set("Cache-Control", cacheControl) if contentType == "image/svg+xml" { w.Header().Set("Content-Security-Policy", svgContentSecurityPolicy) } http.ServeContent(w, r, cleaned, stat.ModTime(), content) } func requestedPath(r *http.Request) string { if rctx := chi.RouteContext(r.Context()); rctx != nil && rctx.RoutePath != "" { return rctx.RoutePath } return r.URL.Path } func cleanPath(p string) string { return strings.TrimPrefix(path.Clean("/"+p), "/") } func (h *Handler) openFile(p string) (fs.File, fs.FileInfo, bool) { if p == "" || p == "." { return nil, nil, false } file, err := h.uploadDir.Open(p) if err != nil { return nil, nil, false } stat, err := file.Stat() if err != nil || stat.IsDir() { file.Close() return nil, nil, false } return file, stat, true } func contentTypeFor(p string) string { ext := path.Ext(p) if contentType, ok := contentTypeExtensions[ext]; ok { return contentType } return fallbackContentType }