package engine // Directory Index import ( "bytes" "net/http" "os" "path/filepath" "strings" "sync" "time" "github.com/go-gcfg/gcfg" "github.com/sirupsen/logrus" "github.com/xyproto/algernon/platformdep" "github.com/xyproto/algernon/themes" "github.com/xyproto/algernon/utils" ) const ( dotSlash = "." + utils.Pathsep /* ./ */ doubleP = utils.Pathsep + utils.Pathsep /* // */ ) // List of filenames that should be displayed instead of a directory listing var indexFilenames = []string{"index.lua", "index.html", "index.md", "index.txt", "index.pongo2", "index.tmpl", "index.po2", "index.amber", "index.jsx", "index.tsx", "index.happ", "index.hyper", "index.hyper.js", "index.hyper.jsx", "index.tl", "index.prompt"} // DirConfig keeps a directory listing configuration type DirConfig struct { Main struct { Title string Theme string } } // dirConfigEntry is a cached parsed .algernon configuration with mtime type dirConfigEntry struct { mtime time.Time config DirConfig } // dirConfigCache caches parsed .algernon configurations by filename and mtime type dirConfigCache struct { entries map[string]dirConfigEntry mu sync.RWMutex } // DirectoryListing serves the given directory as a web page with links the the contents func (ac *Config) DirectoryListing(w http.ResponseWriter, req *http.Request, rootdir, dirname, theme string) { var ( buf bytes.Buffer fullFilename string URLpath string title = dirname ) // Remove a trailing slash after the root directory, if present rootdir = strings.TrimSuffix(rootdir, "/") // Read ignore patterns if ignore.txt is present var ignorePatterns []string ignoreFilePath := filepath.Join(dirname, platformdep.IgnoreFilename) if ac.fs.Exists(ignoreFilePath) { patterns, err := ReadIgnoreFile(ignoreFilePath) if err != nil { logrus.Warn("Could not read ignore.txt: ", err) } else { ignorePatterns = patterns } } // Fill the coming HTML body with a list of all the filenames in `dirname` for _, filename := range utils.GetFilenames(dirname) { if filename == platformdep.DirConfFilename || filename == platformdep.IgnoreFilename { // Skip continue } // Skip dotfiles/dot-directories in listings if --hide-dotfiles is set if ac.hideDotfiles && strings.HasPrefix(filename, ".") { continue } // Check if the filename matches any of the ignore patterns if ShouldIgnore(filename, ignorePatterns) { continue } // Find the full name fullFilename = dirname // Add a "/" after the directory name, if missing if !strings.HasSuffix(fullFilename, utils.Pathsep) { fullFilename += utils.Pathsep } // Add the filename at the end fullFilename += filename // Remove the root directory from the link path URLpath = fullFilename[len(rootdir)+1:] // Output different entries for files and directories buf.WriteString(themes.HTMLLinkWithBase(filename, ac.dirBaseURL, URLpath, ac.fs.IsDir(fullFilename))) } // Read directory configuration, if present if fullDirConfFilename := filepath.Join(dirname, platformdep.DirConfFilename); ac.fs.Exists(fullDirConfFilename) { dirConf := ac.readDirConfig(fullDirConfFilename) if dirConf.Main.Title != "" { title = dirConf.Main.Title } if dirConf.Main.Theme != "" { theme = dirConf.Main.Theme } } else { // Strip the leading "./" from the current directory title = strings.TrimPrefix(title, dotSlash) // Replace "//" with just "/" title = strings.ReplaceAll(title, doubleP, utils.Pathsep) } // Check if the current page contents are empty if buf.Len() == 0 { buf.WriteString("Empty directory") } htmldata := themes.MessagePageBytes(title, buf.Bytes(), theme) // If the auto-refresh feature has been enabled if ac.autoRefresh { // Insert JavaScript for refreshing the page into the generated HTML htmldata = ac.InsertAutoRefresh(req, htmldata) } // Serve the page w.Header().Add(contentType, htmlUTF8) ac.DataToClient(w, req, dirname, htmldata) } // DirPage serves a directory, using index.* files, if present. // The directory must exist. // rootdir is the base directory (can be ".") // dirname is the specific directory that is to be served (should never be ".") func (ac *Config) DirPage(w http.ResponseWriter, req *http.Request, rootdir, dirname, theme, luaDataFilename string) { // Check if we are instructed to quit after serving the first file if ac.quitAfterFirstRequest { go ac.quitSoon("Quit after first request", defaultSoonDuration) } // If the URL does not end with a slash, redirect to an URL that does if !strings.HasSuffix(req.URL.Path, "/") { if req.Method == "POST" { logrus.Warn("Redirecting a POST request: " + req.URL.Path + " -> " + req.URL.Path + "/.") logrus.Warn("Header data may be lost! Please add the missing slash.") } http.Redirect(w, req, req.URL.Path+"/", http.StatusMovedPermanently) return } // Handle the serving of index files, if needed var filename string for _, indexfile := range indexFilenames { filename = filepath.Join(dirname, indexfile) if ac.fs.Exists(filename) && (!ac.strictSymlinks || utils.ResolvesWithinDir(rootdir, filename)) { ac.FilePage(w, req, filename, luaDataFilename) return } } // Serve handler.lua, if found in parent directories (within server root only) rootAbs, err := filepath.Abs(rootdir) if err != nil { rootAbs = rootdir } ancestor, err := filepath.Abs(dirname) if err != nil { ancestor = dirname } for { // Stop before leaving the configured server root rel, relErr := filepath.Rel(rootAbs, ancestor) if relErr != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { break } filename = filepath.Join(ancestor, "handler.lua") if ac.fs.Exists(filename) && (!ac.strictSymlinks || utils.ResolvesWithinDir(rootAbs, filename)) { ac.FilePage(w, req, filename, luaDataFilename) return } if ancestor == rootAbs { break } parent := filepath.Dir(ancestor) if parent == ancestor { // hit filesystem root break } ancestor = parent } // Serve a directory listing if no index file is found ac.DirectoryListing(w, req, rootdir, dirname, theme) } // readDirConfig reads and parses the .algernon configuration file, caching the // parsed result by filename and mtime. Returns an empty DirConfig if the file // cannot be read or parsed. func (ac *Config) readDirConfig(filename string) DirConfig { // Check mtime info, err := os.Stat(filename) if err != nil { return DirConfig{} } mtime := info.ModTime() // Return cached entry if mtime has not changed if ac.dirConfCache != nil { ac.dirConfCache.mu.RLock() if entry, ok := ac.dirConfCache.entries[filename]; ok && entry.mtime.Equal(mtime) { ac.dirConfCache.mu.RUnlock() return entry.config } ac.dirConfCache.mu.RUnlock() } // Read and parse var dirConf DirConfig block, err := ac.cache.Read(filename, ac.shouldCache(".algernon")) if err != nil { return dirConf } gcfg.ReadStringInto(&dirConf, string(block.Bytes())) // Store in cache if ac.dirConfCache == nil { ac.dirConfCache = &dirConfigCache{entries: make(map[string]dirConfigEntry)} } ac.dirConfCache.mu.Lock() ac.dirConfCache.entries[filename] = dirConfigEntry{config: dirConf, mtime: mtime} ac.dirConfCache.mu.Unlock() return dirConf }