Skip to content

Commit f103c57

Browse files
committed
Fixes #373
1 parent b809256 commit f103c57

File tree

12 files changed

+116
-75
lines changed

12 files changed

+116
-75
lines changed

internal/frontend/http.go

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ package frontend
22

33
import (
44
"embed"
5+
"encoding/hex"
56
"fmt"
7+
"hash"
68
"html/template"
79
"net/http"
810
"os"
911
"path"
1012

13+
"github.com/gofrs/uuid/v5"
1114
"github.com/scribble-rs/scribble.rs/internal/translations"
1215
)
1316

@@ -31,6 +34,9 @@ func init() {
3134
// BasePageConfig is data that all pages require to function correctly, no matter
3235
// whether error page or lobby page.
3336
type BasePageConfig struct {
37+
checksums map[string]string
38+
hash hash.Hash
39+
3440
// Version is the tagged source code version of this build. Can be empty for dev
3541
// builds. Untagged commits will be of format `tag-N-gSHA`.
3642
Version string `json:"version"`
@@ -45,10 +51,37 @@ type BasePageConfig struct {
4551
// domain. So it could be https://painting.com. This is required for some
4652
// non critical functionality, such as metadata tags.
4753
RootURL string `json:"rootUrl"`
48-
// CacheBust is a string that is appended to all resources to prevent
49-
// browsers from using cached data of a previous version, but still have
50-
// long lived max age values.
51-
CacheBust string `json:"cacheBust"`
54+
}
55+
56+
var fallbackChecksum = uuid.Must(uuid.NewV4()).String()
57+
58+
func (baseConfig *BasePageConfig) Hash(key string, bytes []byte) error {
59+
_, alreadyExists := baseConfig.checksums[key]
60+
if alreadyExists {
61+
return fmt.Errorf("duplicate hash key '%s'", key)
62+
}
63+
if _, err := baseConfig.hash.Write(bytes); err != nil {
64+
return fmt.Errorf("error hashing '%s': %w", key, err)
65+
}
66+
baseConfig.checksums[key] = hex.EncodeToString(baseConfig.hash.Sum(nil))
67+
baseConfig.hash.Reset()
68+
return nil
69+
}
70+
71+
// CacheBust is a string that is appended to all resources to prevent
72+
// browsers from using cached data of a previous version, but still have
73+
// long lived max age values.
74+
func (baseConfig *BasePageConfig) withCacheBust(file string) string {
75+
checksum, found := baseConfig.checksums[file]
76+
if !found {
77+
// No need to crash over
78+
return fmt.Sprintf("%s?cache_bust=%s", file, fallbackChecksum)
79+
}
80+
return fmt.Sprintf("%s?cache_bust=%s", file, checksum)
81+
}
82+
83+
func (baseConfig *BasePageConfig) WithCacheBust(file string) template.HTMLAttr {
84+
return template.HTMLAttr(baseConfig.withCacheBust(file))
5285
}
5386

5487
func (handler *SSRHandler) cspMiddleware(handleFunc http.HandlerFunc) http.HandlerFunc {
@@ -108,8 +141,8 @@ func (handler *SSRHandler) SetupRoutes(register func(string, string, http.Handle
108141
}),
109142
).ServeHTTP,
110143
)
111-
register("GET", path.Join(handler.cfg.RootPath, "lobbyJs"), handler.lobbyJs)
112-
register("GET", path.Join(handler.cfg.RootPath, "indexJs"), handler.indexJs)
144+
register("GET", path.Join(handler.cfg.RootPath, "lobby.js"), handler.lobbyJs)
145+
register("GET", path.Join(handler.cfg.RootPath, "index.js"), handler.indexJs)
113146
registerWithCsp("GET", path.Join(handler.cfg.RootPath, "ssrEnterLobby", "{lobby_id}"), handler.ssrEnterLobby)
114147
registerWithCsp("POST", path.Join(handler.cfg.RootPath, "ssrCreateLobby"), handler.ssrCreateLobby)
115148
}

internal/frontend/index.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ package frontend
22

33
import (
44
//nolint:gosec //We just use this for cache busting, so it's secure enough
5+
56
"crypto/md5"
6-
"encoding/hex"
77
"fmt"
88
"log"
99
"net/http"
@@ -45,9 +45,11 @@ type SSRHandler struct {
4545

4646
func NewHandler(cfg *config.Config) (*SSRHandler, error) {
4747
basePageConfig := &BasePageConfig{
48-
Version: version.Version,
49-
Commit: version.Commit,
50-
RootURL: cfg.RootURL,
48+
checksums: make(map[string]string),
49+
hash: md5.New(),
50+
Version: version.Version,
51+
Commit: version.Commit,
52+
RootURL: cfg.RootURL,
5153
}
5254
if cfg.RootPath != "" {
5355
basePageConfig.RootPath = "/" + cfg.RootPath
@@ -75,19 +77,22 @@ func NewHandler(cfg *config.Config) (*SSRHandler, error) {
7577
}
7678

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

85-
if _, err := hash.Write(bytes); err != nil {
86+
if err := basePageConfig.Hash(entry.Name(), bytes); err != nil {
8687
return nil, fmt.Errorf("error hashing resource %s: %w", entry.Name(), err)
8788
}
8889
}
89-
90-
basePageConfig.CacheBust = hex.EncodeToString(hash.Sum(nil))
90+
if err := basePageConfig.Hash("index.js", []byte(indexJsRaw)); err != nil {
91+
return nil, fmt.Errorf("error hashing: %w", err)
92+
}
93+
if err := basePageConfig.Hash("lobby.js", []byte(lobbyJsRaw)); err != nil {
94+
return nil, fmt.Errorf("error hashing: %w", err)
95+
}
9196

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

109-
writer.Header().Add("Content-Type", "text/javascript")
114+
writer.Header().Set("Content-Type", "text/javascript")
115+
// Duration of 1 year, since we use cachebusting anyway.
116+
writer.Header().Set("Cache-Control", "public, max-age=31536000")
110117
writer.WriteHeader(http.StatusOK)
111118
if err := handler.indexJsRawTemplate.ExecuteTemplate(writer, "index-js", pageData); err != nil {
112119
log.Printf("error templating JS: %s\n", err)

internal/frontend/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,13 @@ const set_lobbies = (lobbies, visible) => {
165165
return element;
166166
};
167167
const user_pair = create_info_pair(
168-
"{{.RootPath}}/resources/user.svg?cache_bust={{.CacheBust}}",
168+
`{{.RootPath}}/resources/{{.WithCacheBust "user.svg"}}`,
169169
`${lobby.playerCount}/${lobby.maxPlayers}`);
170170
const round_pair = create_info_pair(
171-
"{{.RootPath}}/resources/round.svg?cache_bust={{.CacheBust}}",
171+
`{{.RootPath}}/resources/{{.WithCacheBust "round.svg"}}`,
172172
`${lobby.round}/${lobby.rounds}`);
173173
const time_pair = create_info_pair(
174-
"{{.RootPath}}/resources/clock.svg?cache_bust={{.CacheBust}}",
174+
`{{.RootPath}}/resources/{{.WithCacheBust "clock.svg"}}`,
175175
`${lobby.drawingTime}`);
176176

177177
lobby_list_row_b.replaceChildren(user_pair, round_pair, time_pair);

internal/frontend/lobby.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ func (handler *SSRHandler) lobbyJs(writer http.ResponseWriter, request *http.Req
4242
Locale: locale,
4343
}
4444

45-
writer.Header().Add("Content-Type", "text/javascript")
45+
writer.Header().Set("Content-Type", "text/javascript")
46+
// Duration of 1 year, since we use cachebusting anyway.
47+
writer.Header().Set("Cache-Control", "public, max-age=31536000")
4648
writer.WriteHeader(http.StatusOK)
4749
if err := handler.lobbyJsRawTemplate.ExecuteTemplate(writer, "lobby-js", pageData); err != nil {
4850
log.Printf("error templating JS: %s\n", err)

internal/frontend/lobby.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -386,9 +386,9 @@ document.getElementById("toggle-sound-button").addEventListener("click", toggleS
386386

387387
function updateSoundIcon() {
388388
if (sound) {
389-
soundToggleLabel.src = "{{.RootPath}}/resources/sound.svg?cache_bust={{.CacheBust}}";
389+
soundToggleLabel.src = `{{.RootPath}}/resources/{{.WithCacheBust "sound.svg"}}`;
390390
} else {
391-
soundToggleLabel.src = "{{.RootPath}}/resources/no-sound.svg?cache_bust={{.CacheBust}}";
391+
soundToggleLabel.src = `{{.RootPath}}/resources/{{.WithCacheBust "no-sound.svg"}}`;
392392
}
393393
}
394394

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

402402
function updateTogglePenIcon() {
403403
if (penPressure) {
404-
penToggleLabel.src = "{{.RootPath}}/resources/pen.svg?cache_bust={{.CacheBust}}";
404+
penToggleLabel.src = `{{.RootPath}}/resources/{{.WithCacheBust "pen.svg"}}`;
405405
} else {
406-
penToggleLabel.src = "{{.RootPath}}/resources/no-pen.svg?cache_bust={{.CacheBust}}";
406+
penToggleLabel.src = `{{.RootPath}}/resources/{{.WithCacheBust "no-pen.svg"}}`;
407407
}
408408
}
409409

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

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

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

933933
clear(context);
934934

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

950950
setAllowDrawing(false);
951951
} else if (parsed.type === "your-turn") {
952-
playWav('{{.RootPath}}/resources/your-turn.wav?cache_bust={{.CacheBust}}');
952+
playWav('{{.RootPath}}/resources/{{.WithCacheBust "your-turn.wav"}}');
953953
//This dialog could potentially stay visible from last
954954
//turn, in case nobody has chosen a word.
955955
waitChooseDialog.style.visibility = "hidden";
@@ -1297,11 +1297,11 @@ function applyPlayers(players) {
12971297
if (player.state === "standby") {
12981298
playerDiv.classList.add("player-done");
12991299
} else if (player.state === "drawing") {
1300-
const playerStateImage = createPlayerStateImageNode("{{.RootPath}}/resources/pencil.svg?cache_bust={{.CacheBust}}");
1300+
const playerStateImage = createPlayerStateImageNode(`{{.RootPath}}/resources/{{.WithCacheBust "pencil.svg"}}`);
13011301
playerStateImage.style.transform = "scaleX(-1)";
13021302
scoreAndStatusDiv.appendChild(playerStateImage);
13031303
} else if (player.state === "standby") {
1304-
const playerStateImage = createPlayerStateImageNode("{{.RootPath}}/resources/checkmark.svg?cache_bust={{.CacheBust}}");
1304+
const playerStateImage = createPlayerStateImageNode(`{{.RootPath}}/resources/{{.WithCacheBust "checkmark.svg"}}`);
13051305
scoreAndStatusDiv.appendChild(playerStateImage);
13061306
}
13071307
} else {

internal/frontend/templates/error.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
<title>Scribble.rs - Error</title>
77
<meta charset="UTF-8" />
88
{{template "non-static-css-decl" .}}
9-
<link rel="stylesheet" type="text/css" href="{{.RootPath}}/resources/root.css?cache_bust={{.CacheBust}}" />
10-
<link rel="stylesheet" type="text/css" href="{{.RootPath}}/resources/error.css?cache_bust={{.CacheBust}}" />
9+
<link rel="stylesheet" type="text/css" href='{{.RootPath}}/resources/{{.WithCacheBust "root.css"}}' />
10+
<link rel="stylesheet" type="text/css" href='{{.RootPath}}/resources/{{.WithCacheBust "error.css"}}' />
1111
{{template "favicon-decl" .}}
1212
</head>
1313

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{{define "favicon-decl"}}
2-
<link rel="icon" type="image/svg+xml" href="{{.RootPath}}/resources/favicon.svg?cache_bust={{.CacheBust}}" sizes="any">
3-
<link rel="icon" type="image/png" sizes="16x16" href="{{.RootPath}}/resources/favicon_16.png?cache_bust={{.CacheBust}}">
4-
<link rel="icon" type="image/png" sizes="32x32" href="{{.RootPath}}/resources/favicon_32.png?cache_bust={{.CacheBust}}">
5-
<link rel="icon" type="image/png" sizes="92x92" href="{{.RootPath}}/resources/favicon_92.png?cache_bust={{.CacheBust}}">
6-
{{end}}
2+
<link rel="icon" type="image/svg+xml" href='{{.RootPath}}/resources/{{.WithCacheBust "favicon.svg"}}' sizes="any">
3+
<link rel="icon" type="image/png" sizes="16x16" href='{{.RootPath}}/resources/{{.WithCacheBust "favicon_16.png"}}'>
4+
<link rel="icon" type="image/png" sizes="32x32" href='{{.RootPath}}/resources/{{.WithCacheBust "favicon_32.png"}}'>
5+
<link rel="icon" type="image/png" sizes="92x92" href='{{.RootPath}}/resources/{{.WithCacheBust "favicon_92.png"}}'>
6+
{{end}}

internal/frontend/templates/index.html

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,18 @@
2222
<meta name="twitter:card" content="summary_large_image">
2323
{{end}}
2424
{{template "non-static-css-decl" .}}
25-
<link rel="stylesheet" type="text/css" href="{{.RootPath}}/resources/root.css?cache_bust={{.CacheBust}}" />
26-
<link rel="stylesheet" type="text/css" href="{{.RootPath}}/resources/index.css?cache_bust={{.CacheBust}}" />
25+
<link rel="stylesheet" type="text/css" href='{{.RootPath}}/resources/{{.WithCacheBust "root.css"}}' />
26+
<link rel="stylesheet" type="text/css" href='{{.RootPath}}/resources/{{.WithCacheBust "index.css"}}' />
2727
{{template "favicon-decl" .}}
28-
<link rel="prefetch" href="{{.RootPath}}/resources/user.svg?cache_bust={{.CacheBust}}" />
29-
<link rel="prefetch" href="{{.RootPath}}/resources/round.svg?cache_bust={{.CacheBust}}" />
30-
<link rel="prefetch" href="{{.RootPath}}/resources/clock.svg?cache_bust={{.CacheBust}}" />
28+
<link rel="prefetch" href='{{.RootPath}}/resources/{{.WithCacheBust "user.svg"}}' />
29+
<link rel="prefetch" href='{{.RootPath}}/resources/{{.WithCacheBust "round.svg"}}' />
30+
<link rel="prefetch" href='{{.RootPath}}/resources/{{.WithCacheBust "clock.svg"}}' />
3131
</head>
3232

3333
<body>
3434
<div id="app">
3535
<div class="home">
36-
<img id="logo" src="{{.RootPath}}/resources/logo.svg?cache_bust={{.CacheBust}}" alt="Scribble.rs logo">
36+
<img id="logo" src='{{.RootPath}}/resources/{{.WithCacheBust "logo.svg"}}' alt="Scribble.rs logo">
3737
<div id="home-choices">
3838
<div class="home-choice">
3939
<div class="home-choice-inner">
@@ -166,7 +166,7 @@
166166
{{template "footer" .}}
167167
</footer>
168168

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

172172
</html>

0 commit comments

Comments
 (0)