/
static.go
137 lines (120 loc) · 3.71 KB
/
static.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
132
133
134
135
136
137
package server
import (
"errors"
"io/fs"
"net/http"
"path"
"strconv"
"strings"
"github.com/golang/gddo/httputil/header"
)
const cacheControlStatic = "max-age=0,stale-while-revalidate=604800"
func setCacheControl(w http.ResponseWriter, r *http.Request, noCache bool) {
if noCache {
w.Header().Set("Cache-Control", "no-cache")
} else {
w.Header().Set("Cache-Control", cacheControlStatic)
}
}
// Based on
// https://cs.opensource.google/go/go/+/refs/tags/go1.18:src/net/http/fs.go
// https://github.com/lpar/gzipped/blob/v1.1.0/fileserver.go
type staticHandler struct {
fs http.FileSystem
noCache bool
}
// NewStaticHandler tries to serve the original files, but if they are not found,
// it will serve a compressed version of the file. Usually this is reversed, but in heedy,
// the build process actually *removes* the original files, so when the compressed files exist,
// the originals are not present. It also sets up the correct caching headers for static files
func NewStaticHandler(root http.FileSystem, nocache bool) http.Handler {
return &staticHandler{fs: root, noCache: nocache}
}
// https://github.com/lpar/gzipped/blob/v1.1.0/fileserver.go#L45
func acceptable(r *http.Request, encoding string) bool {
for _, aspec := range header.ParseAccept(r.Header, "Accept-Encoding") {
if aspec.Value == encoding && aspec.Q == 0.0 {
return false
}
if (aspec.Value == encoding || aspec.Value == "*") && aspec.Q > 0.0 {
return true
}
}
return false
}
func (pch *staticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
name := r.URL.Path
if !strings.HasPrefix(name, "/") {
name = "/" + name
r.URL.Path = name
}
name = path.Clean(name)
if strings.HasSuffix(name, "/") {
http.NotFound(w, r)
return
}
f, err := pch.fs.Open(name)
isgzip := false
if errors.Is(err, fs.ErrNotExist) {
// We don't accept range queries over the gzipped files.
rangeHeader := r.Header.Get("Range")
if rangeHeader != "" {
http.Error(w, "406 Not Acceptable", http.StatusNotAcceptable)
return
}
f, err = pch.fs.Open(name + ".gz")
if err == nil {
// The gzipped file was found. Now check if the client can actually accept it...
if !acceptable(r, "gzip") {
http.Error(w, "406 Not Acceptable", http.StatusNotAcceptable)
return
}
isgzip = true
}
}
if err != nil {
msg, code := toHTTPError(err)
http.Error(w, msg, code)
return
}
defer f.Close()
d, err := f.Stat()
if err != nil {
msg, code := toHTTPError(err)
http.Error(w, msg, code)
return
}
// Is it a folder?
if d.IsDir() {
// We don't allow directory listing.
http.NotFound(w, r)
return
}
if isgzip {
// ServeContent doesn't actually set the content length if there is a content-encoding header.
w.Header().Set("Content-Length", strconv.FormatInt(d.Size(), 10))
w.Header().Set("Content-Encoding", "gzip")
}
// Set caching headers
if pch.noCache {
w.Header().Set("Cache-Control", "no-cache")
} else {
w.Header().Set("Cache-Control", cacheControlStatic)
}
http.ServeContent(w, r, name, d.ModTime(), f)
}
// toHTTPError returns a non-specific HTTP error message and status code
// for a given non-nil error value. It's important that toHTTPError does not
// actually return err.Error(), since msg and httpStatus are returned to users,
// and historically Go's ServeContent always returned just "404 Not Found" for
// all errors. We don't want to start leaking information in error messages.
func toHTTPError(err error) (msg string, httpStatus int) {
if errors.Is(err, fs.ErrNotExist) {
return "404 page not found", http.StatusNotFound
}
if errors.Is(err, fs.ErrPermission) {
return "403 Forbidden", http.StatusForbidden
}
// Default:
return "500 Internal Server Error", http.StatusInternalServerError
}