Skip to content

Commit

Permalink
Fixes #373
Browse files Browse the repository at this point in the history
  • Loading branch information
Bios-Marcel committed Dec 26, 2024
1 parent b809256 commit f1e0ae5
Show file tree
Hide file tree
Showing 11 changed files with 115 additions and 74 deletions.
45 changes: 39 additions & 6 deletions internal/frontend/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package frontend

import (
"embed"
"encoding/hex"
"fmt"
"hash"
"html/template"
"net/http"
"os"
"path"

"github.com/gofrs/uuid/v5"
"github.com/scribble-rs/scribble.rs/internal/translations"
)

Expand All @@ -31,6 +34,9 @@ func init() {
// BasePageConfig is data that all pages require to function correctly, no matter
// whether error page or lobby page.
type BasePageConfig struct {
checksums map[string]string
hash hash.Hash

// Version is the tagged source code version of this build. Can be empty for dev
// builds. Untagged commits will be of format `tag-N-gSHA`.
Version string `json:"version"`
Expand All @@ -45,10 +51,37 @@ type BasePageConfig struct {
// domain. So it could be https://painting.com. This is required for some
// non critical functionality, such as metadata tags.
RootURL string `json:"rootUrl"`
// CacheBust is a string that is appended to all resources to prevent
// browsers from using cached data of a previous version, but still have
// long lived max age values.
CacheBust string `json:"cacheBust"`
}

var fallbackChecksum = uuid.Must(uuid.NewV4()).String()

func (baseConfig *BasePageConfig) Hash(key string, bytes []byte) error {
_, alreadyExists := baseConfig.checksums[key]
if alreadyExists {
return fmt.Errorf("duplicate hash key '%s'")
}
if _, err := baseConfig.hash.Write(bytes); err != nil {
return fmt.Errorf("error hashing '%s': %w", key, err)
}
baseConfig.checksums[key] = hex.EncodeToString(baseConfig.hash.Sum(nil))
baseConfig.hash.Reset()
return nil
}

// CacheBust is a string that is appended to all resources to prevent
// browsers from using cached data of a previous version, but still have
// long lived max age values.
func (baseConfig *BasePageConfig) withCacheBust(file string) string {
checksum, found := baseConfig.checksums[file]
if !found {
// No need to crash over
return fmt.Sprintf("%s?cache_bust=%s", file, fallbackChecksum)
}
return fmt.Sprintf("%s?cache_bust=%s", file, checksum)
}

func (baseConfig *BasePageConfig) WithCacheBust(file string) template.HTMLAttr {
return template.HTMLAttr(baseConfig.withCacheBust(file))
}

func (handler *SSRHandler) cspMiddleware(handleFunc http.HandlerFunc) http.HandlerFunc {
Expand Down Expand Up @@ -108,8 +141,8 @@ func (handler *SSRHandler) SetupRoutes(register func(string, string, http.Handle
}),
).ServeHTTP,
)
register("GET", path.Join(handler.cfg.RootPath, "lobbyJs"), handler.lobbyJs)
register("GET", path.Join(handler.cfg.RootPath, "indexJs"), handler.indexJs)
register("GET", path.Join(handler.cfg.RootPath, "lobby.js"), handler.lobbyJs)
register("GET", path.Join(handler.cfg.RootPath, "index.js"), handler.indexJs)
registerWithCsp("GET", path.Join(handler.cfg.RootPath, "ssrEnterLobby", "{lobby_id}"), handler.ssrEnterLobby)
registerWithCsp("POST", path.Join(handler.cfg.RootPath, "ssrCreateLobby"), handler.ssrCreateLobby)
}
Expand Down
25 changes: 16 additions & 9 deletions internal/frontend/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package frontend

import (
//nolint:gosec //We just use this for cache busting, so it's secure enough

"crypto/md5"
"encoding/hex"
"fmt"
"log"
"net/http"
Expand Down Expand Up @@ -45,9 +45,11 @@ type SSRHandler struct {

func NewHandler(cfg *config.Config) (*SSRHandler, error) {
basePageConfig := &BasePageConfig{
Version: version.Version,
Commit: version.Commit,
RootURL: cfg.RootURL,
checksums: make(map[string]string),
hash: md5.New(),
Version: version.Version,
Commit: version.Commit,
RootURL: cfg.RootURL,
}
if cfg.RootPath != "" {
basePageConfig.RootPath = "/" + cfg.RootPath
Expand Down Expand Up @@ -75,19 +77,22 @@ func NewHandler(cfg *config.Config) (*SSRHandler, error) {
}

//nolint:gosec //We just use this for cache busting, so it's secure enough
hash := md5.New()
for _, entry := range entries {
bytes, err := frontendResourcesFS.ReadFile("resources/" + entry.Name())
if err != nil {
return nil, fmt.Errorf("error reading resource %s: %w", entry.Name(), err)
}

if _, err := hash.Write(bytes); err != nil {
if err := basePageConfig.Hash(entry.Name(), bytes); err != nil {
return nil, fmt.Errorf("error hashing resource %s: %w", entry.Name(), err)
}
}

basePageConfig.CacheBust = hex.EncodeToString(hash.Sum(nil))
if err := basePageConfig.Hash("index.js", []byte(indexJsRaw)); err != nil {
return nil, fmt.Errorf("error hashing: %w", err)
}
if err := basePageConfig.Hash("lobby.js", []byte(lobbyJsRaw)); err != nil {
return nil, fmt.Errorf("error hashing: %w", err)
}

handler := &SSRHandler{
cfg: cfg,
Expand All @@ -106,7 +111,9 @@ func (handler *SSRHandler) indexJs(writer http.ResponseWriter, request *http.Req
Locale: locale,
}

writer.Header().Add("Content-Type", "text/javascript")
writer.Header().Set("Content-Type", "text/javascript")
// Duration of 1 year, since we use cachebusting anyway.
writer.Header().Set("Cache-Control", "public, max-age=31536000")
writer.WriteHeader(http.StatusOK)
if err := handler.indexJsRawTemplate.ExecuteTemplate(writer, "index-js", pageData); err != nil {
log.Printf("error templating JS: %s\n", err)
Expand Down
6 changes: 3 additions & 3 deletions internal/frontend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,13 @@ const set_lobbies = (lobbies, visible) => {
return element;
};
const user_pair = create_info_pair(
"{{.RootPath}}/resources/user.svg?cache_bust={{.CacheBust}}",
`{{.RootPath}}/resources/{{.WithCacheBust "user.svg"}}`,
`${lobby.playerCount}/${lobby.maxPlayers}`);
const round_pair = create_info_pair(
"{{.RootPath}}/resources/round.svg?cache_bust={{.CacheBust}}",
`{{.RootPath}}/resources/{{.WithCacheBust "round.svg"}}`,
`${lobby.round}/${lobby.rounds}`);
const time_pair = create_info_pair(
"{{.RootPath}}/resources/clock.svg?cache_bust={{.CacheBust}}",
`{{.RootPath}}/resources/{{.WithCacheBust "clock.svg"}}`,
`${lobby.drawingTime}`);

lobby_list_row_b.replaceChildren(user_pair, round_pair, time_pair);
Expand Down
4 changes: 3 additions & 1 deletion internal/frontend/lobby.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ func (handler *SSRHandler) lobbyJs(writer http.ResponseWriter, request *http.Req
Locale: locale,
}

writer.Header().Add("Content-Type", "text/javascript")
writer.Header().Set("Content-Type", "text/javascript")
// Duration of 1 year, since we use cachebusting anyway.
writer.Header().Set("Cache-Control", "public, max-age=31536000")
writer.WriteHeader(http.StatusOK)
if err := handler.lobbyJsRawTemplate.ExecuteTemplate(writer, "lobby-js", pageData); err != nil {
log.Printf("error templating JS: %s\n", err)
Expand Down
18 changes: 9 additions & 9 deletions internal/frontend/lobby.js
Original file line number Diff line number Diff line change
Expand Up @@ -386,9 +386,9 @@ document.getElementById("toggle-sound-button").addEventListener("click", toggleS

function updateSoundIcon() {
if (sound) {
soundToggleLabel.src = "{{.RootPath}}/resources/sound.svg?cache_bust={{.CacheBust}}";
soundToggleLabel.src = `{{.RootPath}}/resources/{{.WithCacheBust "sound.svg"}}`;
} else {
soundToggleLabel.src = "{{.RootPath}}/resources/no-sound.svg?cache_bust={{.CacheBust}}";
soundToggleLabel.src = `{{.RootPath}}/resources/{{.WithCacheBust "no-sound.svg"}}`;
}
}

Expand All @@ -401,9 +401,9 @@ document.getElementById("toggle-pen-pressure-button").addEventListener("click",

function updateTogglePenIcon() {
if (penPressure) {
penToggleLabel.src = "{{.RootPath}}/resources/pen.svg?cache_bust={{.CacheBust}}";
penToggleLabel.src = `{{.RootPath}}/resources/{{.WithCacheBust "pen.svg"}}`;
} else {
penToggleLabel.src = "{{.RootPath}}/resources/no-pen.svg?cache_bust={{.CacheBust}}";
penToggleLabel.src = `{{.RootPath}}/resources/{{.WithCacheBust "no-pen.svg"}}`;
}
}

Expand Down Expand Up @@ -862,7 +862,7 @@ function registerMessageHandler(targetSocket) {
waitChooseDrawerSpan.innerText = parsed.data.playerName;
}
} else if (parsed.type === "correct-guess") {
playWav('{{.RootPath}}/resources/plop.wav?cache_bust={{.CacheBust}}');
playWav('{{.RootPath}}/resources/{{.WithCacheBust "plop.wav"}}');

if (parsed.data === ownID) {
appendMessage("correct-guess-message", null, `{{.Translation.Get "correct-guess"}}`);
Expand Down Expand Up @@ -928,7 +928,7 @@ function registerMessageHandler(targetSocket) {

//If a player doesn't choose, the dialog will still be up.
wordDialog.style.visibility = "hidden";
playWav('{{.RootPath}}/resources/end-turn.wav?cache_bust={{.CacheBust}}');
playWav('{{.RootPath}}/resources/{{.WithCacheBust "end-turn.wav"}}');

clear(context);

Expand All @@ -949,7 +949,7 @@ function registerMessageHandler(targetSocket) {

setAllowDrawing(false);
} else if (parsed.type === "your-turn") {
playWav('{{.RootPath}}/resources/your-turn.wav?cache_bust={{.CacheBust}}');
playWav('{{.RootPath}}/resources/{{.WithCacheBust "your-turn.wav"}}');
//This dialog could potentially stay visible from last
//turn, in case nobody has chosen a word.
waitChooseDialog.style.visibility = "hidden";
Expand Down Expand Up @@ -1297,11 +1297,11 @@ function applyPlayers(players) {
if (player.state === "standby") {
playerDiv.classList.add("player-done");
} else if (player.state === "drawing") {
const playerStateImage = createPlayerStateImageNode("{{.RootPath}}/resources/pencil.svg?cache_bust={{.CacheBust}}");
const playerStateImage = createPlayerStateImageNode(`{{.RootPath}}/resources/{{.WithCacheBust "pencil.svg"}}`);
playerStateImage.style.transform = "scaleX(-1)";
scoreAndStatusDiv.appendChild(playerStateImage);
} else if (player.state === "standby") {
const playerStateImage = createPlayerStateImageNode("{{.RootPath}}/resources/checkmark.svg?cache_bust={{.CacheBust}}");
const playerStateImage = createPlayerStateImageNode(`{{.RootPath}}/resources/{{.WithCacheBust "checkmark.svg"}}`);
scoreAndStatusDiv.appendChild(playerStateImage);
}
} else {
Expand Down
4 changes: 2 additions & 2 deletions internal/frontend/templates/error.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<title>Scribble.rs - Error</title>
<meta charset="UTF-8" />
{{template "non-static-css-decl" .}}
<link rel="stylesheet" type="text/css" href="{{.RootPath}}/resources/root.css?cache_bust={{.CacheBust}}" />
<link rel="stylesheet" type="text/css" href="{{.RootPath}}/resources/error.css?cache_bust={{.CacheBust}}" />
<link rel="stylesheet" type="text/css" href='{{.RootPath}}/resources/{{.WithCacheBust "root.css"}}' />
<link rel="stylesheet" type="text/css" href='{{.RootPath}}/resources/{{.WithCacheBust "error.css"}}' />
{{template "favicon-decl" .}}
</head>

Expand Down
10 changes: 5 additions & 5 deletions internal/frontend/templates/favicon.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{{define "favicon-decl"}}
<link rel="icon" type="image/svg+xml" href="{{.RootPath}}/resources/favicon.svg?cache_bust={{.CacheBust}}" sizes="any">
<link rel="icon" type="image/png" sizes="16x16" href="{{.RootPath}}/resources/favicon_16.png?cache_bust={{.CacheBust}}">
<link rel="icon" type="image/png" sizes="32x32" href="{{.RootPath}}/resources/favicon_32.png?cache_bust={{.CacheBust}}">
<link rel="icon" type="image/png" sizes="92x92" href="{{.RootPath}}/resources/favicon_92.png?cache_bust={{.CacheBust}}">
{{end}}
<link rel="icon" type="image/svg+xml" href='{{.RootPath}}/resources/{{.WithCacheBust "favicon.svg"}}' sizes="any">
<link rel="icon" type="image/png" sizes="16x16" href='{{.RootPath}}/resources/{{.WithCacheBust "favicon_16.png"}}'>
<link rel="icon" type="image/png" sizes="32x32" href='{{.RootPath}}/resources/{{.WithCacheBust "favicon_32.png"}}'>
<link rel="icon" type="image/png" sizes="92x92" href='{{.RootPath}}/resources/{{.WithCacheBust "favicon_92.png"}}'>
{{end}}
14 changes: 7 additions & 7 deletions internal/frontend/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,18 @@
<meta name="twitter:card" content="summary_large_image">
{{end}}
{{template "non-static-css-decl" .}}
<link rel="stylesheet" type="text/css" href="{{.RootPath}}/resources/root.css?cache_bust={{.CacheBust}}" />
<link rel="stylesheet" type="text/css" href="{{.RootPath}}/resources/index.css?cache_bust={{.CacheBust}}" />
<link rel="stylesheet" type="text/css" href='{{.RootPath}}/resources/{{.WithCacheBust "root.css"}}' />
<link rel="stylesheet" type="text/css" href='{{.RootPath}}/resources/{{.WithCacheBust "index.css"}}' />
{{template "favicon-decl" .}}
<link rel="prefetch" href="{{.RootPath}}/resources/user.svg?cache_bust={{.CacheBust}}" />
<link rel="prefetch" href="{{.RootPath}}/resources/round.svg?cache_bust={{.CacheBust}}" />
<link rel="prefetch" href="{{.RootPath}}/resources/clock.svg?cache_bust={{.CacheBust}}" />
<link rel="prefetch" href='{{.RootPath}}/resources/{{.WithCacheBust "user.svg"}}' />
<link rel="prefetch" href='{{.RootPath}}/resources/{{.WithCacheBust "round.svg"}}' />
<link rel="prefetch" href='{{.RootPath}}/resources/{{.WithCacheBust "clock.svg"}}' />
</head>

<body>
<div id="app">
<div class="home">
<img id="logo" src="{{.RootPath}}/resources/logo.svg?cache_bust={{.CacheBust}}" alt="Scribble.rs logo">
<img id="logo" src='{{.RootPath}}/resources/{{.WithCacheBust "logo.svg"}}' alt="Scribble.rs logo">
<div id="home-choices">
<div class="home-choice">
<div class="home-choice-inner">
Expand Down Expand Up @@ -166,7 +166,7 @@
{{template "footer" .}}
</footer>

<script type="text/javascript" src="{{.RootPath}}/indexJs?cache_bust={{.CacheBust}}"></script>
<script type="text/javascript" src='{{.RootPath}}/{{.WithCacheBust "index.js"}}'></script>
</body>

</html>
Expand Down
Loading

0 comments on commit f1e0ae5

Please sign in to comment.