package http import "log" import "fmt" import "path" import "io/fs" import "strings" import "strconv" import "net/http" import "path/filepath" import "git.tebibyte.media/sashakoshka/step" import "git.tebibyte.media/sashakoshka/goutil/container" type ErrorData struct { Status int Message any } type Handler struct { Environment *step.Environment Directories bool StepExt ucontainer.Set[string] Index []string ErrorDocument string DirectoryDocument string } func (this *Handler) ServeHTTP (res http.ResponseWriter, req *http.Request) { remoteAddr := req.RemoteAddr if addr := req.Header.Get("CF-Connecting-IP"); addr != "" { remoteAddr = fmt.Sprintf("%s --CF-> %s", addr, req.RemoteAddr) } else if addr := req.Header.Get("X-Forwarded-For"); addr != "" { remoteAddr = fmt.Sprintf("%s --??-> %s", addr, req.RemoteAddr) } log.Println("(i)", req.Method, req.URL, "from", remoteAddr) filesystem := this.Environment.GetFS() // normalize path pat := req.URL.Path if !strings.HasPrefix(pat, "/") { pat = "/" + pat req.URL.Path = pat } hasTrailingSlash := strings.HasSuffix(pat, "/") pat = path.Clean(req.URL.Path) info, err := statFile(filesystem, pathToName(pat)) if err != nil { this.serveError(res, req, http.StatusNotFound, req.URL, false) return } if info.IsDir() { // ensure the path ends with a / if !hasTrailingSlash { http.Redirect(res, req, pat + "/", http.StatusMovedPermanently) return } // try to find an index for _, base := range this.Index { currentPath := path.Join(pat, base) info, err := statFile(filesystem, pathToName(currentPath)) if err != nil { continue } if info.IsDir() { continue } this.serveFile(res, req, currentPath) return } if !this.Directories { this.serveError(res, req, http.StatusForbidden, req.URL, false) return } // TODO: serve a directory and return // have a custom directory listing document that takes in a list // of the files in the directory and the path to the directory // as data } this.serveFile(res, req, pat) } func (this *Handler) serveFile ( res http.ResponseWriter, req *http.Request, pat string, ) { name := pathToName(pat) if !this.StepExt.Has(filepath.Ext(name)) { // just a normal file http.ServeFileFS(res, req, this.Environment.GetFS(), name) return } this.serveDocument(res, req, name) } func (this *Handler) serveDocument ( res http.ResponseWriter, req *http.Request, name string, ) { // parse document, err := this.Environment.Parse(name) if err != nil { this.serveError(res, req, http.StatusInternalServerError, err, false) return } // set up HTTP response recorder recorder := HTTPResponseRecorder { } resetRecorder := func () { recorder.Reset() recorder.Head = res.Header().Clone() } if contentType, ok := document.Meta["content-type"]; ok { recorder.Header().Set("Content-Type", contentType) } if status, ok := document.Meta["status"]; ok { if status, err := strconv.Atoi(status); err == nil { recorder.Status = status } } // execute document data := HTTPData { } data.Res = WrappedResponseWriter { responseWriter: &recorder, resetFunc: resetRecorder, Header: WrappedHeader { Header: recorder.Header(), }, } data.Req = req err = document.Execute(&recorder, step.ExecutionData { Data: data, }) if err != nil { this.serveError(res, req, http.StatusInternalServerError, err, false) return } // play back recorded response err = recorder.Play(res) if err != nil { this.logErr(name, err) } } func (this *Handler) serveError ( res http.ResponseWriter, req *http.Request, status int, message any, safeMode bool, ) { log.Printf("ERR %d %s: %v\n", status, http.StatusText(status), message) if safeMode || this.ErrorDocument == "" { res.Header().Add("Content-Type", "text/plain") res.WriteHeader(status) // TODO: allow customization with templates if message == nil { fmt.Fprintf(res, "%d %s\n", status, http.StatusText(status)) } else { fmt.Fprintf(res, "%d %s: %v\n", status, http.StatusText(status), message) } return } document, err := this.Environment.Parse(this.ErrorDocument) if err != nil { this.serveError(res, req, http.StatusInternalServerError, err, true) return } err = document.Execute(res, step.ExecutionData { Data: ErrorData { Status: status, Message: message, }, }) if err != nil { this.serveError(res, req, http.StatusInternalServerError, err, true) return } } func (this *Handler) logErr (name string, err error) { log.Printf("ERR %s: %v\n", name, err) } func statFile (filesystem fs.FS, name string) (fs.FileInfo, error) { if filesystem, ok := filesystem.(fs.StatFS); ok { return filesystem.Stat(name) } file, err := filesystem.Open(name) if err != nil { return nil, err } defer file.Close() return file.Stat() } func pathToName (pat string) string { if strings.HasPrefix(pat, "/") { pat = strings.TrimPrefix(pat, "/") } if pat == "" { return "." } return pat }