about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorAlan Pearce2024-05-30 13:54:29 +0200
committerAlan Pearce2024-05-30 13:54:29 +0200
commit4698a97974ae82e7bd8592828c58294b222a58ff (patch)
treefc7d6357534efffc69301fb01c6e04143288dbec
parentb02076363f979daa6ac313058eb140d1d67ce184 (diff)
downloadsearchix-4698a97974ae82e7bd8592828c58294b222a58ff.tar.lz
searchix-4698a97974ae82e7bd8592828c58294b222a58ff.tar.zst
searchix-4698a97974ae82e7bd8592828c58294b222a58ff.zip
feat: enable sub-resource integrity for assets
-rw-r--r--frontend/assets.go95
-rw-r--r--frontend/templates/blocks/search.gotmpl8
-rw-r--r--frontend/templates/index.gotmpl9
-rw-r--r--internal/server/mux.go4
4 files changed, 113 insertions, 3 deletions
diff --git a/frontend/assets.go b/frontend/assets.go
new file mode 100644
index 0000000..a6a5e79
--- /dev/null
+++ b/frontend/assets.go
@@ -0,0 +1,95 @@
+package frontend
+
+import (
+	"crypto/sha256"
+	"encoding/base64"
+	"io"
+	"io/fs"
+	"net/url"
+
+	"github.com/pkg/errors"
+)
+
+var Assets = AssetCollection{
+	Scripts:     make(map[string]Asset),
+	Stylesheets: make(map[string]Asset),
+}
+
+type Asset struct {
+	URL          string
+	Base64SHA256 string
+}
+
+type AssetCollection struct {
+	Scripts     map[string]Asset
+	Stylesheets map[string]Asset
+}
+
+func hashFile(filename string) ([]byte, error) {
+	file, err := Files.Open(filename)
+	if err != nil {
+		return nil, errors.WithMessagef(err, "could not open file %s", filename)
+	}
+	sum := sha256.New()
+	defer file.Close()
+	if _, err := io.Copy(sum, file); err != nil {
+		return nil, errors.WithMessagef(err, "could not hash file %s", filename)
+	}
+
+	return sum.Sum(nil), nil
+}
+
+func newAsset(filename string, hash []byte) Asset {
+	u, err := url.JoinPath("/", filename)
+	if err != nil {
+		panic(err)
+	}
+
+	return Asset{
+		URL:          u,
+		Base64SHA256: base64.StdEncoding.EncodeToString(hash),
+	}
+}
+
+func hashScripts() error {
+	scripts, err := fs.Glob(Files, "static/**.js")
+	if err != nil {
+		return errors.WithMessage(err, "could not glob files")
+	}
+	for _, filename := range scripts {
+		hash, err := hashFile(filename)
+		if err != nil {
+			return err
+		}
+		Assets.Scripts[filename] = newAsset(filename, hash)
+	}
+
+	return nil
+}
+
+func hashStyles() error {
+	styles, err := fs.Glob(Files, "static/**.css")
+	if err != nil {
+		return errors.WithMessage(err, "could not glob files")
+	}
+	for _, filename := range styles {
+		hash, err := hashFile(filename)
+		if err != nil {
+			return err
+		}
+		Assets.Stylesheets[filename] = newAsset(filename, hash)
+	}
+
+	return nil
+}
+
+func init() {
+	err := hashScripts()
+	if err != nil {
+		panic(err)
+	}
+	err = hashStyles()
+	if err != nil {
+		panic(err)
+	}
+}
diff --git a/frontend/templates/blocks/search.gotmpl b/frontend/templates/blocks/search.gotmpl
index 1be001a..9320376 100644
--- a/frontend/templates/blocks/search.gotmpl
+++ b/frontend/templates/blocks/search.gotmpl
@@ -22,5 +22,11 @@
 {{- end }}
 
 {{- define "head" }}
-  <script src="/static/search.js" defer></script>
+  {{- with (index .Assets.Scripts "static/search.js") }}
+    <script
+      src="{{ .URL }}"
+      defer
+      integrity="sha256-{{ .Base64SHA256 }}"
+    ></script>
+  {{- end }}
 {{- end }}
diff --git a/frontend/templates/index.gotmpl b/frontend/templates/index.gotmpl
index ee37c0f..7732dc8 100644
--- a/frontend/templates/index.gotmpl
+++ b/frontend/templates/index.gotmpl
@@ -4,8 +4,13 @@
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <title>Searchix</title>
-    <link href="/static/base.css" rel="stylesheet" />
-    <link href="/static/style.css" rel="stylesheet" />
+    {{- range .Assets.Stylesheets }}
+      <link
+        href="{{ .URL }}"
+        rel="stylesheet"
+        integrity="sha256-{{ .Base64SHA256 }}"
+      />
+    {{- end }}
     {{ block "head" . }}
     {{ end }}
     {{ .ExtraHeadHTML }}
diff --git a/internal/server/mux.go b/internal/server/mux.go
index ea4b70c..87e878a 100644
--- a/internal/server/mux.go
+++ b/internal/server/mux.go
@@ -43,6 +43,7 @@ type TemplateData struct {
 	ExtraHeadHTML template.HTML
 	Code          int
 	Message       string
+	Assets        frontend.AssetCollection
 }
 
 type ResultData struct {
@@ -94,6 +95,7 @@ func NewMux(
 		indexData := TemplateData{
 			ExtraHeadHTML: cfg.Web.ExtraHeadHTML,
 			Sources:       cfg.Importer.Sources,
+			Assets:        frontend.Assets,
 		}
 		w.Header().Add("Cache-Control", "max-age=86400")
 		err := templates["index"].Execute(w, indexData)
@@ -142,6 +144,7 @@ func NewMux(
 						ExtraHeadHTML: cfg.Web.ExtraHeadHTML,
 						Source:        *source,
 						Sources:       cfg.Importer.Sources,
+						Assets:        frontend.Assets,
 					},
 					ResultsPerPage: search.ResultsPerPage,
 					Query:          qs,
@@ -205,6 +208,7 @@ func NewMux(
 					Sources:       cfg.Importer.Sources,
 					Source:        *source,
 					SourceResult:  sourceResult,
+					Assets:        frontend.Assets,
 				})
 				if err != nil {
 					errorHandler(w, r, err.Error(), http.StatusInternalServerError)