// Copyright (C) 2022 Marius Schellenberger package srv import ( "errors" "io" stdfs "io/fs" "math" "net/http" "path" "strings" "cachefs/pkg/fs" "github.com/go-logr/logr" ) const ( csp = "Content-Security-Policy" indexCSP = "default-src 'none';style-src 'unsafe-inline';frame-ancestors 'none'" videoCSP = indexCSP + ";script-src 'sha256-ZLSc/s5/US9uVMMZOJ/yWiS1tj9nEoi+5Qoohy3QetU=';media-src 'self'" gib = 1024 * 1024 * 1024 ) type data struct { QuotaCur float64 QuotaEst float64 QuotaMax float64 Paths dirContents PathAnchor string } type FileServer struct { log logr.Logger fs *fs.FS h http.Handler } func NewFileServer(fs *fs.FS, log logr.Logger) http.Handler { return &FileServer{log, fs, http.FileServer(fs)} } func skipLog(path string) bool { return path == "/?o=preloads" || strings.HasSuffix(path, "favicon.ico") } func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { s := &statusInterceptor{w: w} w = s if !skipLog(r.URL.Path) { defer func() { if s.Status() == http.StatusPartialContent { return } fs.log.Info("access", "client", r.RemoteAddr, "method", r.Method, "status", s.Status(), "uri", r.RequestURI, ) }() } upath := r.URL.Path if !strings.HasPrefix(upath, "/") { upath = "/" + upath r.URL.Path = upath } p := path.Clean(upath) option := r.FormValue("o") d, err := fs.fs.Stat(p) if err != nil { msg, code := toHTTPError(err) http.Error(w, msg, code) return } h := w.Header() if option == "v" { h.Set(csp, videoCSP) err = video.Execute(w, data{Paths: dirContents{Base: r.URL.Path}}) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return } if !d.IsDir() { if option == "p" || option == "i" || option == "d" || option == "s" { switch option { case "p": fs.fs.Preload(p, 0) case "i": fs.fs.Preload(p, 1) case "d": fs.fs.Preload(p, -1) case "s": fs.fs.RemovePreload(p) } rdir := "/" if r.FormValue("r") == "preloads" { http.Redirect(w, r, rdir+"?o=preloads", http.StatusFound) return } if i := strings.LastIndex(p, "/"); i > 0 { rdir = p[:i] } http.Redirect(w, r, rdir+"#"+anchor(p), http.StatusFound) return } if option == "n" { fs.fs.NoCache.ServeHTTP(w, r) return } fs.h.ServeHTTP(w, r) return } if option == "preloads" { h.Set(csp, indexCSP) cur, max := fs.fs.QuotaUsage() paths, size := getPreloads(p, fs.fs) err = preloads.Execute(w, data{ QuotaCur: math.Round(float64(cur)/gib*100) / 100, QuotaEst: math.Round(float64(cur+size)/gib*100) / 100, QuotaMax: float64(max) / gib, Paths: paths, }) if err != nil { fs.log.Error(err, "error rendering preloads") http.Error(w, err.Error(), http.StatusInternalServerError) } return } i := &responseInterceptor{w: w} r.Header.Del("If-Modified-Since") r.Header.Del("Cache-Control") fs.h.ServeHTTP(i, r) h.Del("Last-Modified") paths, pathAnchor, err := i.GetPaths(p, fs.fs, false) if err == io.EOF { w.WriteHeader(i.Status()) return } if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } h.Set(csp, indexCSP) w.WriteHeader(i.Status()) err = index.Execute(w, data{Paths: paths, PathAnchor: pathAnchor}) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } func toHTTPError(err error) (string, int) { if errors.Is(err, stdfs.ErrNotExist) { return "404 page not found", http.StatusNotFound } if errors.Is(err, stdfs.ErrPermission) { return "403 Forbidden", http.StatusForbidden } return "500 Internal Server Error", http.StatusInternalServerError }