Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · server/web/instance_assets.go

Line attribution on main · 83 line(s)

6ce635a8
Keenan Nemetz
1d ago
1 package web
2
3 // The instance repository's static files. A theme's CSS, favicon and assets are
ed03b3d1
Keenan Nemetz
2h ago
4 // served from /-/static/custom/ before the embedded set is consulted, so the URL
6ce635a8
Keenan Nemetz
1d ago
5 // namespace is one: a theme file never has to shadow a built-in path, and a miss
6 // is still the embedded 404.
7 //
8 // The response is keyed by the commit it came from: the URL carries ?v=<commit>,
9 // and the ETag is that commit. A push moves the commit and every custom URL with
10 // it, so a browser never has to revalidate a file that cannot have changed.
11
12 import (
13 "bytes"
14 "net/http"
15 "path"
16 "strings"
17 "time"
18 )
19
ed03b3d1
Keenan Nemetz
2h ago
20 const instanceAssetPrefix = "/-/static/custom/"
6ce635a8
Keenan Nemetz
1d ago
21
22 // Static serves the instance repository's files first, then the embedded assets.
23 func (w *Web) Static() http.Handler {
24 embedded := Static()
25 return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
26 if w.serveInstanceAsset(rw, r) {
27 return
28 }
29 embedded.ServeHTTP(rw, r)
30 })
31 }
32
ed03b3d1
Keenan Nemetz
2h ago
33 // serveInstanceAsset answers a request under /-/static/custom/ and reports whether
6ce635a8
Keenan Nemetz
1d ago
34 // it did. It never builds a filesystem path: the snapshot map is the whole
35 // lookup, so a traversal is simply a miss.
36 func (w *Web) serveInstanceAsset(rw http.ResponseWriter, r *http.Request) bool {
37 if w.InstanceRepo == nil || !strings.HasPrefix(r.URL.Path, instanceAssetPrefix) {
38 return false
39 }
40 snap := w.InstanceRepo.Current()
41 if snap == nil {
42 return false
43 }
44 rel := strings.TrimPrefix(r.URL.Path, instanceAssetPrefix)
45 var body []byte
46 switch {
47 case rel == "profile/custom.css":
48 body = snap.CSS
49 case rel == "profile/favicon.svg":
50 body = snap.Favicon
f7a34d87
Keenan Nemetz
1d ago
51 case strings.HasPrefix(rel, "themes/") && strings.HasSuffix(rel, ".css"):
52 id := strings.TrimSuffix(strings.TrimPrefix(rel, "themes/"), ".css")
53 for _, t := range snap.Themes {
54 if t.ID == id {
55 body = t.CSS
56 break
57 }
58 }
6ce635a8
Keenan Nemetz
1d ago
59 case strings.HasPrefix(rel, "assets/"):
60 if a, ok := snap.Assets[strings.TrimPrefix(rel, "assets/")]; ok {
61 body = a.Body
62 }
63 }
64 if len(body) == 0 {
65 return false
66 }
67 v := snap.Commit
68 if len(v) > 12 {
69 v = v[:12]
70 }
71 rw.Header().Set("ETag", `"`+v+`"`)
72 rw.Header().Set("Cache-Control", "no-cache")
73 rw.Header().Set("X-Content-Type-Options", "nosniff")
74 if strings.HasSuffix(rel, ".svg") {
75 // An SVG opened directly is a document, and a document can carry script.
76 // The CSP makes it inert; as an <img> it renders normally.
77 rw.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
78 }
79 // ServeContent answers If-None-Match from the ETag and picks the content type
80 // from the name; an unlisted file never gets this far.
81 http.ServeContent(rw, r, path.Base(rel), time.Time{}, bytes.NewReader(body))
82 return true
83 }