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: 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

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"
To build the web server, you'll need to install the Go compiler, and run `make`
from the _xP_ directory. Then start it from the _public_ subdirectory,
and navigate to the adress you gave it as its first argument--in the following
example, that would be http://localhost:8080[]:
To build the web server, install the Go compiler, and run `make`
from the _xP_ directory. Then start the resulting binary, and navigate to
the adress you give it as its first argument--in the following example,
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,
and some form of HTTP authentication. Pass the external URL of the WebSocket

View File

@ -1,6 +1,6 @@
module janouch.name/xK/xP
go 1.21
go 1.22
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
package main
@ -6,12 +6,16 @@ package main
import (
"bufio"
"context"
"crypto/sha1"
"embed"
"encoding/binary"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"html/template"
"io"
"io/fs"
"log"
"net"
"net/http"
@ -23,7 +27,12 @@ import (
)
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
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>
<html>
<head>
<title>xP</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<base href="{{ .Root }}/">
<link rel="stylesheet" href="xP.css" />
</head>
<body>
<script src="mithril.js">
</script>
<script>
let proxy = '{{ . }}'
let proxy = '{{ .Proxy }}'
</script>
<script type="module" src="xP.js">
</script>
@ -262,20 +270,49 @@ var page = template.Must(template.New("/").Parse(`<!DOCTYPE html>
</html>`))
func handleDefault(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
staticHandler.ServeHTTP(w, r)
return
}
wsURI := addressWS
if wsURI == "" {
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())
}
}
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() {
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(),
@ -294,6 +331,21 @@ func main() {
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("/", http.HandlerFunc(handleDefault))