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.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 18m ago 4b8c9e1d
history blame raw 2.6 KiB
1package web
2
3// The instance repository's static files. A theme's CSS, favicon and assets are
4// served from /-/static/custom/ before the embedded set is consulted, so the URL
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
12import (
13 "bytes"
14 "net/http"
15 "path"
16 "strings"
17 "time"
18)
19
20const instanceAssetPrefix = "/-/static/custom/"
21
22// Static serves the instance repository's files first, then the embedded assets.
23func (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
33// serveInstanceAsset answers a request under /-/static/custom/ and reports whether
34// it did. It never builds a filesystem path: the snapshot map is the whole
35// lookup, so a traversal is simply a miss.
36func (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
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 }
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}
84