+89
src/http.go
+89
src/http.go
···
1
+
package git_pages
2
+
3
+
import (
4
+
"cmp"
5
+
"regexp"
6
+
"slices"
7
+
"strconv"
8
+
"strings"
9
+
)
10
+
11
+
var httpAcceptEncodingRegexp = regexp.MustCompile(`` +
12
+
// token optionally prefixed by whitespace
13
+
`^[ \t]*([a-zA-Z0-9$!#$%&'*+.^_\x60|~-]+)` +
14
+
// quality value prefixed by a semicolon optionally surrounded by whitespace
15
+
`(?:[ \t]*;[ \t]*q=(0(?:\.[0-9]{1,3})?|1(?:\.0{1,3})?))?` +
16
+
// optional whitespace followed by comma or end of line
17
+
`[ \t]*(?:,|$)`,
18
+
)
19
+
20
+
type httpEncoding struct {
21
+
code string
22
+
qval float64
23
+
}
24
+
25
+
type httpEncodings struct {
26
+
encodings []httpEncoding
27
+
}
28
+
29
+
func parseHTTPEncodings(headerValue string) (result httpEncodings) {
30
+
for headerValue != "" {
31
+
matches := httpAcceptEncodingRegexp.FindStringSubmatch(headerValue)
32
+
if matches == nil {
33
+
return httpEncodings{}
34
+
}
35
+
enc := httpEncoding{strings.ToLower(matches[1]), 1.0}
36
+
if matches[2] != "" {
37
+
enc.qval, _ = strconv.ParseFloat(matches[2], 64)
38
+
}
39
+
result.encodings = append(result.encodings, enc)
40
+
headerValue = headerValue[len(matches[0]):]
41
+
}
42
+
if len(result.encodings) == 0 {
43
+
// RFC 9110 says (https://httpwg.org/specs/rfc9110.html#field.accept-encoding):
44
+
// "If no Accept-Encoding header field is in the request, any content
45
+
// coding is considered acceptable by the user agent."
46
+
// In practice, no client expects to receive a compressed response
47
+
// without having sent Accept-Encoding in the request.
48
+
}
49
+
return
50
+
}
51
+
52
+
// Negotiate returns the most preferred encoding that is acceptable by the
53
+
// client, or an empty string if no encodings are acceptable.
54
+
func (e *httpEncodings) Negotiate(codes ...string) string {
55
+
prefs := make(map[string]float64, len(codes))
56
+
for _, code := range codes {
57
+
prefs[code] = 0
58
+
}
59
+
implicitIdentity := true
60
+
for _, enc := range e.encodings {
61
+
if enc.code == "*" {
62
+
for code := range prefs {
63
+
prefs[code] = enc.qval
64
+
}
65
+
implicitIdentity = false
66
+
} else if _, ok := prefs[enc.code]; ok {
67
+
prefs[enc.code] = enc.qval
68
+
}
69
+
if enc.code == "*" || enc.code == "identity" {
70
+
implicitIdentity = false
71
+
}
72
+
}
73
+
if _, ok := prefs["identity"]; ok && implicitIdentity {
74
+
prefs["identity"] = -1 // sort last
75
+
}
76
+
encs := make([]httpEncoding, len(codes))
77
+
for idx, code := range codes {
78
+
encs[idx] = httpEncoding{code, prefs[code]}
79
+
}
80
+
slices.SortStableFunc(encs, func(a, b httpEncoding) int {
81
+
return -cmp.Compare(a.qval, b.qval)
82
+
})
83
+
for _, enc := range encs {
84
+
if enc.qval != 0 {
85
+
return enc.code
86
+
}
87
+
}
88
+
return ""
89
+
}
+41
-12
src/pages.go
+41
-12
src/pages.go
···
13
13
"net/url"
14
14
"os"
15
15
"path"
16
+
"strconv"
16
17
"strings"
17
18
"time"
18
19
···
229
230
defer closer.Close()
230
231
}
231
232
233
+
acceptedEncodings := parseHTTPEncodings(r.Header.Get("Accept-Encoding"))
234
+
negotiatedEncoding := true
235
+
232
236
switch entry.GetTransform() {
233
237
case Transform_None:
234
-
// nothing to do
238
+
if acceptedEncodings.Negotiate("identity") != "identity" {
239
+
negotiatedEncoding = false
240
+
}
235
241
case Transform_Zstandard:
236
-
// Ideally, we would serve zstd-compressed data to a client that indicates support with
237
-
// an `Accept-Encoding: zstd` header. Unfortunately we can't because we rely on MIME
238
-
// type detection done in `http.ServeContent`.
239
-
compressedData, _ := io.ReadAll(reader)
240
-
decompressedData, err := zstdDecoder.DecodeAll(compressedData, []byte{})
241
-
if err != nil {
242
-
w.WriteHeader(http.StatusInternalServerError)
243
-
fmt.Fprintf(w, "internal server error: %s\n", err)
244
-
return err
242
+
supported := []string{"zstd", "identity"}
243
+
if entry.ContentType == nil {
244
+
// If Content-Type is unset, `http.ServeContent` will try to sniff
245
+
// the file contents. That won't work if it's compressed.
246
+
supported = []string{"identity"}
245
247
}
246
-
reader = bytes.NewReader(decompressedData)
248
+
switch acceptedEncodings.Negotiate(supported...) {
249
+
case "zstd":
250
+
// Set Content-Length ourselves since `http.ServeContent` only sets
251
+
// it if Content-Encoding is unset or if it's a range request.
252
+
w.Header().Set("Content-Length", strconv.FormatInt(*entry.Size, 10))
253
+
w.Header().Set("Content-Encoding", "zstd")
254
+
case "identity":
255
+
compressedData, _ := io.ReadAll(reader)
256
+
decompressedData, err := zstdDecoder.DecodeAll(compressedData, []byte{})
257
+
if err != nil {
258
+
w.WriteHeader(http.StatusInternalServerError)
259
+
fmt.Fprintf(w, "internal server error: %s\n", err)
260
+
return err
261
+
}
262
+
reader = bytes.NewReader(decompressedData)
263
+
default:
264
+
negotiatedEncoding = false
265
+
}
266
+
}
267
+
if !negotiatedEncoding {
268
+
w.WriteHeader(http.StatusNotAcceptable)
269
+
return fmt.Errorf("no supported content encodings (accept-encoding: %q)",
270
+
r.Header.Get("Accept-Encoding"))
247
271
}
248
272
249
273
// decide on the HTTP status
···
253
277
io.Copy(w, reader)
254
278
}
255
279
} else {
280
+
if entry.ContentType != nil {
281
+
// don't let http.ServeContent mime-sniff compressed data
282
+
w.Header().Set("Content-Type", *entry.ContentType)
283
+
}
284
+
256
285
// allow the use of multi-threading in WebAssembly
257
286
w.Header().Set("Cross-Origin-Embedder-Policy", "credentialless")
258
287
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
···
265
294
w.Header().Set("Cache-Control", "max-age=60, stale-while-revalidate=3600")
266
295
// see https://web.dev/articles/stale-while-revalidate for details
267
296
268
-
// http.ServeContent handles content type and caching
297
+
// http.ServeContent handles conditional requests and range requests
269
298
http.ServeContent(w, r, entryPath, mtime, reader)
270
299
}
271
300
return nil