oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1package home
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "net/http"
7 "regexp"
8 "strings"
9 "sync"
10)
11
12// Asset versioning. Home's stylesheets and pictures are served at stable
13// paths (a closed list, mountBrand and friends), and every page links
14// them as path?v=<content version>. serveAsset caches a request carrying
15// the current version for a year, immutably, and revalidates anything
16// else, so a returning visitor fetches only the page, and a deploy still
17// takes effect at once: new bytes, new version, new URL.
18//
19// Scripts are linked the same way where a template can (busy.js); the
20// passkey and handoff scripts keep bare paths their own modules import,
21// revalidated by ETag. noscript_test.go's closed list names them all.
22
23// contentVersion is the first 16 hex digits of the body's SHA-256.
24func contentVersion(body []byte) string {
25 sum := sha256.Sum256(body)
26 return hex.EncodeToString(sum[:8])
27}
28
29var (
30 assetVersionsMu sync.RWMutex
31 assetVersions = map[string]string{}
32)
33
34// versionAsset records path's current version. Called where each asset is
35// mounted; every router mounts the same bytes, so the value never changes.
36func versionAsset(path string, body []byte) {
37 assetVersionsMu.Lock()
38 assetVersions[path] = contentVersion(body)
39 assetVersionsMu.Unlock()
40}
41
42// versioned is path with its version, or path unchanged if it is not a
43// versioned asset (a script, an estilo URL already versioned in its path).
44func versioned(path string) string {
45 assetVersionsMu.RLock()
46 v, ok := assetVersions[path]
47 assetVersionsMu.RUnlock()
48 if !ok {
49 return path
50 }
51 return path + "?v=" + v
52}
53
54var staticRef = regexp.MustCompile(`((?:href|src)=")(/static/[^"?#]+)"`)
55
56// versionStaticRefs rewrites every href/src naming a versioned asset in a
57// static page (the front page, which is bytes, not a template).
58func versionStaticRefs(page []byte) []byte {
59 return staticRef.ReplaceAllFunc(page, func(m []byte) []byte {
60 parts := staticRef.FindSubmatch(m)
61 return []byte(string(parts[1]) + versioned(string(parts[2])) + `"`)
62 })
63}
64
65// etagMatches is If-None-Match's weak comparison (RFC 9110 §13.1.2):
66// any listed tag, W/ or not, naming the same opaque value matches, and so
67// does "*". Weak matters here: whatever gzips oficina.works on the way out
68// turns this binary's strong tag into W/"...", and an exact compare would
69// then never answer 304.
70func etagMatches(header, etag string) bool {
71 want := strings.TrimPrefix(etag, "W/")
72 for _, tag := range strings.Split(header, ",") {
73 tag = strings.TrimSpace(tag)
74 if tag == "*" || strings.TrimPrefix(tag, "W/") == want {
75 return true
76 }
77 }
78 return false
79}
80
81// mountCSS mounts a versioned stylesheet.
82func mountCSS(r interface {
83 Get(string, http.HandlerFunc)
84}, path string, body []byte) {
85 versionAsset(path, body)
86 r.Get(path, serveAsset(body, "text/css; charset=utf-8"))
87}
88