-
Notifications
You must be signed in to change notification settings - Fork 3
/
filesystem.go
128 lines (119 loc) · 3.67 KB
/
filesystem.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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)
})
}