xP: embed web resources, tame browser caching
All checks were successful
Alpine 3.21 Success
Arch Linux AUR Success
OpenBSD 7.6 Success

This commit is contained in:
Přemysl Eric Janouch 2025-07-09 21:53:19 +02:00
parent 80af5c22d6
commit 71e1a744c5
Signed by: p
GPG Key ID: A0420B94F92B9493
4 changed files with 73 additions and 17 deletions

4
NEWS
View File

@ -7,6 +7,10 @@ Unreleased
* xP: added a network lag indicator to the user interface * xP: added a network lag indicator to the user interface
* xP: started embedding the necessary web resources,
and making sure that the files have unique paths after change,
so that stale copies are not cached by browsers indefinitely
* Bumped relay protocol version * Bumped relay protocol version

View File

@ -136,12 +136,12 @@ The precondition for running 'xC' frontends is enabling its relay interface:
/set general.relay_bind = "127.0.0.1:9000" /set general.relay_bind = "127.0.0.1:9000"
To build the web server, you'll need to install the Go compiler, and run `make` To build the web server, install the Go compiler, and run `make`
from the _xP_ directory. Then start it from the _public_ subdirectory, from the _xP_ directory. Then start the resulting binary, and navigate to
and navigate to the adress you gave it as its first argument--in the following the adress you give it as its first argument--in the following example,
example, that would be http://localhost:8080[]: that would be http://localhost:8080[]:
$ ../xP 127.0.0.1:8080 127.0.0.1:9000 $ ./xP 127.0.0.1:8080 127.0.0.1:9000
For remote use, it's recommended to put 'xP' behind a reverse proxy, with TLS, For remote use, it's recommended to put 'xP' behind a reverse proxy, with TLS,
and some form of HTTP authentication. Pass the external URL of the WebSocket and some form of HTTP authentication. Pass the external URL of the WebSocket

View File

@ -1,6 +1,6 @@
module janouch.name/xK/xP module janouch.name/xK/xP
go 1.21 go 1.22
toolchain go1.23.2 toolchain go1.23.2

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name> // Copyright (c) 2022 - 2025, Přemysl Eric Janouch <p@janouch.name>
// SPDX-License-Identifier: 0BSD // SPDX-License-Identifier: 0BSD
package main package main
@ -6,12 +6,16 @@ package main
import ( import (
"bufio" "bufio"
"context" "context"
"crypto/sha1"
"embed"
"encoding/binary" "encoding/binary"
"encoding/hex"
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
"io/fs"
"log" "log"
"net" "net"
"net/http" "net/http"
@ -24,6 +28,11 @@ import (
var ( var (
debug = flag.Bool("debug", false, "enable debug output") debug = flag.Bool("debug", false, "enable debug output")
webRoot = flag.String("webroot", "", "override bundled web resources")
//go:embed public/*
webResources embed.FS
webResourcesHash string
addressBind string addressBind string
addressConnect string addressConnect string
@ -240,21 +249,20 @@ func handleWS(w http.ResponseWriter, r *http.Request) {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
var staticHandler = http.FileServer(http.Dir("."))
var page = template.Must(template.New("/").Parse(`<!DOCTYPE html> var page = template.Must(template.New("/").Parse(`<!DOCTYPE html>
<html> <html>
<head> <head>
<title>xP</title> <title>xP</title>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<base href="{{ .Root }}/">
<link rel="stylesheet" href="xP.css" /> <link rel="stylesheet" href="xP.css" />
</head> </head>
<body> <body>
<script src="mithril.js"> <script src="mithril.js">
</script> </script>
<script> <script>
let proxy = '{{ . }}' let proxy = '{{ .Proxy }}'
</script> </script>
<script type="module" src="xP.js"> <script type="module" src="xP.js">
</script> </script>
@ -262,20 +270,49 @@ var page = template.Must(template.New("/").Parse(`<!DOCTYPE html>
</html>`)) </html>`))
func handleDefault(w http.ResponseWriter, r *http.Request) { func handleDefault(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
staticHandler.ServeHTTP(w, r)
return
}
wsURI := addressWS wsURI := addressWS
if wsURI == "" { if wsURI == "" {
wsURI = fmt.Sprintf("ws://%s/ws", r.Host) wsURI = fmt.Sprintf("ws://%s/ws", r.Host)
} }
if err := page.Execute(w, wsURI); err != nil {
args := struct {
Root string
Proxy string
}{
Root: webResourcesHash,
Proxy: wsURI,
}
if err := page.Execute(w, &args); err != nil {
log.Println("Template execution failed: " + err.Error()) log.Println("Template execution failed: " + err.Error())
} }
} }
func hashFS(root fs.FS) []byte {
hasher := sha1.New()
callback := func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// Note that this can be fooled.
fmt.Fprintln(hasher, path)
if !d.IsDir() {
file, err := root.Open(path)
if err != nil {
return err
}
defer file.Close()
io.Copy(hasher, file)
}
return nil
}
if err := fs.WalkDir(root, ".", callback); err != nil {
log.Fatalln(err)
}
return hasher.Sum(nil)
}
func main() { func main() {
flag.Usage = func() { flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), fmt.Fprintf(flag.CommandLine.Output(),
@ -294,6 +331,21 @@ func main() {
addressWS = flag.Arg(2) addressWS = flag.Arg(2)
} }
subResources, err := fs.Sub(webResources, "public")
if err != nil {
log.Fatalln(err)
}
if *webRoot != "" {
subResources = os.DirFS(*webRoot)
}
// The simplest way of ensuring that web browsers don't use
// stale cached copies of our files.
webResourcesHash = hex.EncodeToString(hashFS(subResources))
http.Handle("/"+webResourcesHash+"/",
http.StripPrefix("/"+webResourcesHash+"/",
http.FileServerFS(subResources)))
http.Handle("/ws", http.HandlerFunc(handleWS)) http.Handle("/ws", http.HandlerFunc(handleWS))
http.Handle("/", http.HandlerFunc(handleDefault)) http.Handle("/", http.HandlerFunc(handleDefault))