129 lines
3.7 KiB
Go
129 lines
3.7 KiB
Go
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)
|
|
})
|
|
}
|