package main import "log" import "fmt" import "path" import "io/fs" import "strings" import "net/http" import "html/template" import "path/filepath" import "git.tebibyte.media/sashakoshka/step" import "git.tebibyte.media/sashakoshka/goutil/container" type handler struct { environment *step.Environment directories bool stepExt ucontainer.Set[string] index []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) // TODO log all this stuff filesystem := this.environment.GetFS() // normalize path pat := req.URL.Path if !strings.HasPrefix(pat, "/") { pat = "/" + pat req.URL.Path = pat } pat = path.Clean(req.URL.Path) info, err := statFile(filesystem, pathToName(pat)) if err != nil { // TODO need more detailed error, should allow specifying error // documents this.serveError(res, req, http.StatusNotFound, req.URL) return } if info.IsDir() { // 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, filesystem, currentPath) return } // TODO: serve a directory // 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 if !this.directories { this.serveError(res, req, http.StatusForbidden, req.URL) return } } this.serveFile(res, req, filesystem, pat) } func (this *handler) serveFile (res http.ResponseWriter, req *http.Request, filesystem fs.FS, 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 } // parse and execute document, err := this.environment.Parse(name) if err != nil { this.serveError(res, req, http.StatusInternalServerError, err) return } err = document.Execute(res, step.ExecutionData { Data: req, }) if err != nil { if tmplErr, ok := err.(*template.Error); ok { log.Printf ( "ERR document %s:%s: %s\n", name, tmplErr.Line, tmplErr.Description) } else { log.Printf("ERR document %s: %v\n", name, err) } } } func (this *handler) serveError (res http.ResponseWriter, req *http.Request, status int, message any) { 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) } log.Printf("ERR %d %s: %v\n", status, http.StatusText(status), message) } 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 }