package gemini import ( "errors" "fmt" "io" "mime" "net/url" "os" "path" "path/filepath" "sort" "strings" "github.com/a-h/gemini/log" ) type Dir string // Open implements FileSystem using os.Open, opening files for reading rooted // and relative to the directory d. func (d Dir) Open(name string) (File, error) { dir := string(d) if dir == "" { dir = "." } fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name))) return os.Open(fullName) } // A FileSystem implements access to a collection of named files. // The elements in a file path are separated by slash ('/', U+002F) // characters, regardless of host operating system convention. type FileSystem interface { Open(name string) (File, error) } // A File is returned by a FileSystem's Open method and can be // served by the FileServer implementation. // // The methods should behave the same as those on an *os.File. type File interface { io.Closer io.Reader Readdir(count int) ([]os.FileInfo, error) Stat() (os.FileInfo, error) } func DirectoryListingHandler(path string, f File) Handler { return HandlerFunc(func(w ResponseWriter, r *Request) { files, err := f.Readdir(-1) if err != nil { log.Warn("DirectoryListingHandler: readdir failed", log.String("reason", err.Error()), log.String("path", r.URL.Path), log.String("url", r.URL.String())) w.SetHeader(CodeTemporaryFailure, "readdir failed") return } sort.Slice(files, func(i, j int) bool { return files[i].Name() < files[j].Name() }) w.SetHeader(CodeSuccess, DefaultMIMEType) fmt.Fprintf(w, "# Index of %s\n\n", path) fmt.Fprintln(w, "=> ../") for _, ff := range files { name := ff.Name() if ff.IsDir() { name += "/" } url := url.URL{Path: name} fmt.Fprintf(w, "=> %v\n", url.String()) } }) } func FileContentHandler(name string, f File) Handler { return HandlerFunc(func(w ResponseWriter, r *Request) { mType := mime.TypeByExtension(path.Ext(name)) if mType == "" { mType = DefaultMIMEType } w.SetHeader(CodeSuccess, mType) if _, err := io.Copy(w, f); err != nil { log.Error("FileContentHandler: failed to write file", err, log.String("fileName", name)) panic("error returning file contents") } }) } func FileSystemHandler(fs FileSystem) Handler { return HandlerFunc(func(w ResponseWriter, r *Request) { if strings.Contains(r.URL.Path, "..") { log.Warn("FileSystemHandler: possible directory traversal attack", log.String("path", r.URL.Path), log.String("url", r.URL.String())) BadRequest(w, r) return } if !strings.HasPrefix(r.URL.Path, "/") { r.URL.Path = "/" + r.URL.Path } f, err := fs.Open(r.URL.Path) if err != nil { if os.IsNotExist(err) { NotFoundHandler().ServeGemini(w, r) return } log.Warn("FileSystemHandler: file open failed", log.String("reason", err.Error()), log.String("path", r.URL.Path), log.String("url", r.URL.String())) w.SetHeader(CodeTemporaryFailure, "file open failed") return } stat, err := f.Stat() if err != nil { log.Warn("FileSystemHandler: file stat failed", log.String("reason", err.Error()), log.String("path", r.URL.Path), log.String("url", r.URL.String())) w.SetHeader(CodeTemporaryFailure, "file stat failed") return } if stat.IsDir() { // Look for index.gmi first before listing contents. if !strings.HasSuffix(r.URL.Path, "/") { RedirectPermanentHandler(r.URL.Path+"/").ServeGemini(w, r) return } index, err := fs.Open(r.URL.Path + "index.gmi") if errors.Is(err, os.ErrNotExist) { DirectoryListingHandler(r.URL.Path, f).ServeGemini(w, r) return } FileContentHandler("index.gmi", index).ServeGemini(w, r) return } FileContentHandler(stat.Name(), f).ServeGemini(w, r) }) }