-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathserver.go
131 lines (114 loc) · 3.34 KB
/
server.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
129
130
131
package main
import (
"embed"
"errors"
"fmt"
"html/template"
"io/fs"
"log/slog"
"math/rand"
"net/http"
"strings"
)
//go:embed templates static
var embedFS embed.FS
type Server struct {
mediaLib *MediaLibrary
tmpl *template.Template
staticVersion string
}
func httpError(r *http.Request, w http.ResponseWriter, err error, code int) {
http.Error(w, err.Error(), code)
slog.Error("failed request",
err,
slog.String("url", r.URL.String()),
slog.Int("code", code),
)
}
// ValidatePath provides a basic protection from the path traversal vulnerability.
func ValidatePath(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "./") || strings.Contains(r.URL.Path, ".\\") {
httpError(r, w, errors.New("invalid path"), http.StatusBadRequest)
return
}
h(w, r)
}
}
// NormalizePath normalizes the request URL by removing the delimeter suffix.
func NormalizePath(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = strings.TrimRight(r.URL.Path, Delimiter)
h(w, r)
}
}
// DisableFileListing disables file listing under directories. It can be used with the built-in http.FileServer.
func DisableFileListing(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/") {
http.NotFound(w, r)
return
}
h.ServeHTTP(w, r)
})
}
type TemplateData struct {
StaticVersion string
*MediaListing
}
func (s *Server) ListingHandler(w http.ResponseWriter, r *http.Request) {
listing, err := s.mediaLib.List(r.URL.Path)
if err != nil {
httpError(r, w, err, http.StatusInternalServerError)
return
}
tmplData := TemplateData{
StaticVersion: s.staticVersion,
MediaListing: listing,
}
if err := s.tmpl.ExecuteTemplate(w, "listing.gohtml", tmplData); err != nil {
httpError(r, w, err, http.StatusInternalServerError)
return
}
}
func (s *Server) StreamHandler(w http.ResponseWriter, r *http.Request) {
url, err := s.mediaLib.ContentURL(r.URL.Path)
if err != nil {
httpError(r, w, err, http.StatusInternalServerError)
return
}
http.Redirect(w, r, url, http.StatusFound)
}
// Don't include sprig just for one function.
var templateFunctions = map[string]any{
"defaultString": func(s string, def string) string {
if s == "" {
return def
}
return s
},
}
// StartServer starts HTTP server.
func StartServer(mediaLib *MediaLibrary, addr string) error {
tmpl, err := template.New("").Funcs(templateFunctions).ParseFS(embedFS, "templates/*.gohtml")
if err != nil {
return err
}
mux := http.NewServeMux()
mux.Handle("/", http.RedirectHandler("/library/", http.StatusMovedPermanently))
staticVersion := fmt.Sprintf("%x", rand.Uint64())
staticFS, err := fs.Sub(embedFS, "static")
if err != nil {
return err
}
staticPath := fmt.Sprintf("/static/%s/", staticVersion)
mux.Handle(staticPath, DisableFileListing(http.StripPrefix(staticPath, http.FileServer(http.FS(staticFS)))))
s := Server{
mediaLib: mediaLib,
tmpl: tmpl,
staticVersion: staticVersion,
}
mux.Handle("/library/", http.StripPrefix("/library/", ValidatePath(NormalizePath(s.ListingHandler))))
mux.Handle("/stream/", http.StripPrefix("/stream/", ValidatePath(NormalizePath(s.StreamHandler))))
return http.ListenAndServe(addr, mux)
}