-
-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathstatic.go
142 lines (127 loc) · 3.39 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
138
139
140
141
142
package zhttp
import (
"fmt"
"io/fs"
"mime"
"net/http"
"os"
"path/filepath"
"strings"
)
// Static file server.
type Static struct {
domain string
cacheControl map[string]int
headers map[string]map[string]string // Headers for specific URLs
files fs.FS
}
// Constants for the NewStatic() cache parameter.
const (
// Don't set any header.
CacheNoHeader = 0
// Set to "no-cache" to tell browsers to always validate a cache (with e.g.
// If-Match or If-None-Match). It does NOT tell browsers to never store a
// cache; use Cache NoStore for that.
CacheNoCache = -1
// Set to "no-store, no-cache" to tell browsers to never store a local copy
// (the no-cache is there to be sure previously stored copies from before
// this header are revalidated).
CacheNoStore = -2
)
// NewStatic returns a new static fileserver.
//
// The domain parameter is used for CORS.
//
// The Cache-Control header is set with the cache parameter, which is a path →
// cache mapping. The path is matched with filepath.Match() and the key "" is
// used if nothing matches. There is no guarantee about the order if multiple
// keys match. One of special Cache* constants can be used.
func NewStatic(domain string, files fs.FS, cache map[string]int) Static {
for k := range cache {
_, err := filepath.Match(k, "")
if err != nil {
panic(fmt.Sprintf("zhttp.NewStatic: invalid pattern in cache map: %s", err))
}
}
return Static{domain: domain, cacheControl: cache, files: files, headers: make(map[string]map[string]string)}
}
var Static404 = func(w http.ResponseWriter, r *http.Request) {
http.Error(w, fmt.Sprintf("file not found: %q", r.RequestURI), 404)
}
// Header sets headers for a path, overriding anything else.
func (s *Static) Header(path string, h map[string]string) {
s.headers[path] = h
}
func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "..") {
http.Error(w, "yeh nah", http.StatusForbidden)
return
}
path := strings.TrimLeft(r.URL.Path, "/")
if path == "" {
path = "index.html"
}
d, err := fs.ReadFile(s.files, path)
if err != nil {
if os.IsNotExist(err) {
Static404(w, r)
return
}
http.Error(w, err.Error(), 500)
return
}
ct := mime.TypeByExtension(filepath.Ext(path))
if ct == "" {
ct = "application/octet-stream"
}
w.Header().Set("Content-Type", ct)
if s.domain != "" {
w.Header().Set("Access-Control-Allow-Origin", s.domain)
}
if s.cacheControl != nil {
cache := -100
c, ok := s.cacheControl[r.URL.Path]
if ok {
cache = c
}
if cache == -100 {
for k, v := range s.cacheControl {
match, _ := filepath.Match(k, r.URL.Path)
if match {
cache = v
break
}
}
}
if cache == -100 {
cache = s.cacheControl["*"]
if cache == 0 {
cache = s.cacheControl[""]
}
}
// TODO: use a more clever scheme where max-age and no-{cache,store} can be
// set independently. For example, we can use the top 3 bits as a bitmask,
// clear those if set, and use the rest as a max-age.
cc := ""
switch cache {
case CacheNoHeader:
cc = ""
case CacheNoCache:
cc = "no-cache"
case CacheNoStore:
cc = "no-store,no-cache"
default:
cc = fmt.Sprintf("public, max-age=%d", cache)
}
if cc != "" {
w.Header().Set("Cache-Control", cc)
}
}
if h, ok := s.headers[r.URL.Path]; ok {
for k, v := range h {
w.Header().Set(k, v)
}
}
w.WriteHeader(200)
w.Write(d)
}