feat: enable sub-resource integrity for assets
Alan Pearce alan@alanpearce.eu
Thu, 30 May 2024 13:54:29 +0200
4 files changed, 113 insertions(+), 3 deletions(-)
A 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) + } +}
M frontend/templates/blocks/search.gotmpl → frontend/templates/blocks/search.gotmpl
@@ -22,5 +22,11 @@ {{- end }} {{- 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 }}
M frontend/templates/index.gotmpl → frontend/templates/index.gotmpl
@@ -4,8 +4,13 @@ <head> <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 }}
M internal/server/mux.go → internal/server/mux.go
@@ -43,6 +43,7 @@ SourceResult *bleve.SearchResult ExtraHeadHTML template.HTML Code int Message string + Assets frontend.AssetCollection } type ResultData struct { @@ -94,6 +95,7 @@ mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { 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 @@ TemplateData: TemplateData{ ExtraHeadHTML: cfg.Web.ExtraHeadHTML, Source: *source, Sources: cfg.Importer.Sources, + Assets: frontend.Assets, }, ResultsPerPage: search.ResultsPerPage, Query: qs, @@ -205,6 +208,7 @@ ExtraHeadHTML: cfg.Web.ExtraHeadHTML, Sources: cfg.Importer.Sources, Source: *source, SourceResult: sourceResult, + Assets: frontend.Assets, }) if err != nil { errorHandler(w, r, err.Error(), http.StatusInternalServerError)