summaryrefslogtreecommitdiff
path: root/internal/web/render.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-23 14:39:31 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-23 14:39:31 +0200
commit4de7f7dca7485c9a6c94f6f86a53d01a974fce54 (patch)
treebcdc1ea366a068310e460804c4a5f04df3ae20f4 /internal/web/render.go
parentb72b81c410c161797eb525a6231b0517b4def993 (diff)
downloadlectio-4de7f7dca7485c9a6c94f6f86a53d01a974fce54.tar.gz
lectio-4de7f7dca7485c9a6c94f6f86a53d01a974fce54.zip
web: HTMX server + lectio-web binary
NewServer wires B1's RenderReadings/Themes/themeCSS/embedded static+ templates FS into an http.ServeMux: GET / (full page), GET /readings (HTMX reading-pane fragment), GET /lookup (bible.Lookup passage search fragment), GET /theme.css (theme stylesheet, falling back through cfg.WebTheme to the built-in default on an unknown name), GET /static/. Run listens on cfg.WebPort (0 = OS-picked free port), prints the URL, best-effort opens a browser, then serves. cmd/lectio-web is the binary entry point (config.Load -> web.Run). Fold-in from the B1 review: hardened themeCSS's name guard to an explicit ^[A-Za-z0-9_-]+$ allowlist (the old filepath.Base/ContainsAny check let ".." through), plus guard-rejection and HTML-escaping regression tests -- B2 is what makes /theme.css?name=<raw> reachable from the network, so it owns closing this out.
Diffstat (limited to 'internal/web/render.go')
-rw-r--r--internal/web/render.go15
1 files changed, 11 insertions, 4 deletions
diff --git a/internal/web/render.go b/internal/web/render.go
index cf72d41..67c7a37 100644
--- a/internal/web/render.go
+++ b/internal/web/render.go
@@ -130,14 +130,21 @@ func Themes() []string {
return names
}
+// themeNameRe is the allowlist a theme name must match: letters, digits,
+// underscore, hyphen only. Legitimate theme stems (built-in or user) already
+// fit this shape; it is deliberately stricter than "no path separators" so
+// it rejects "." / ".." / any other filesystem metacharacter outright
+// instead of trying to enumerate what's unsafe.
+var themeNameRe = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
+
// themeCSS returns one theme's CSS: the user file
// ${XDG_CONFIG_HOME:-~/.config}/lectio/themes/<name>.css if it exists,
// otherwise the embedded static/themes/<name>.css, otherwise an error.
-// name must be a bare file stem (no path separators or "..") -- callers
-// (B2's /theme.css?name= handler) pass this straight through from an HTTP
-// query parameter, so this rejects path traversal rather than trusting it.
+// name must match themeNameRe -- callers (B2's /theme.css?name= handler)
+// pass this straight through from an HTTP query parameter, so this rejects
+// path traversal rather than trusting it.
func themeCSS(name string) ([]byte, error) {
- if name == "" || name != filepath.Base(name) || strings.ContainsAny(name, `/\`) {
+ if !themeNameRe.MatchString(name) {
return nil, fmt.Errorf("web: invalid theme name %q", name)
}