Rework how we bypass browser caches of the scouting app
This patch adds a new way to access files on the webserver. Users can
now request files by their sha256 checksum. The URL pattern follows
`/sha256/<checksum>/<path>`. The `<path>` portion doesn't actually
matter, but it helps with readability.
This patch migrates the main bundle and the pictures to use the new
checksum-addressable scheme.
Signed-off-by: Philipp Schrader <philipp.schrader@gmail.com>
Change-Id: I76eaa9b0f69af98e48e8e73e32c82ce2916fbe41
diff --git a/scouting/webserver/static/BUILD b/scouting/webserver/static/BUILD
index 3166853..0cf22f3 100644
--- a/scouting/webserver/static/BUILD
+++ b/scouting/webserver/static/BUILD
@@ -13,6 +13,7 @@
name = "static_test",
srcs = ["static_test.go"],
data = [
+ "test_pages/index.html",
"test_pages/page.txt",
"test_pages/root.txt",
],
diff --git a/scouting/webserver/static/static.go b/scouting/webserver/static/static.go
index 92d8086..4c46fe7 100644
--- a/scouting/webserver/static/static.go
+++ b/scouting/webserver/static/static.go
@@ -2,7 +2,15 @@
// A year agnostic way to serve static http files.
import (
+ "crypto/sha256"
+ "errors"
+ "fmt"
+ "io"
+ "log"
"net/http"
+ "os"
+ "path/filepath"
+ "strings"
"time"
"github.com/frc971/971-Robot-Code/scouting/webserver/server"
@@ -20,10 +28,98 @@
"X-Accel-Expires": "0",
}
-func NoCache(h http.Handler) http.Handler {
+func MaybeNoCache(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
- for k, v := range noCacheHeaders {
- w.Header().Set(k, v)
+ // We force the browser not to cache index.html so that
+ // browsers will notice when the bundle gets updated.
+ if r.URL.Path == "/" || r.URL.Path == "/index.html" {
+ for k, v := range noCacheHeaders {
+ w.Header().Set(k, v)
+ }
+ }
+
+ h.ServeHTTP(w, r)
+ }
+
+ return http.HandlerFunc(fn)
+}
+
+// Computes the sha256 of the specified file.
+func computeSha256(path string) (string, error) {
+ file, err := os.Open(path)
+ if err != nil {
+ return "", errors.New(fmt.Sprint("Failed to open ", path, ": ", err))
+ }
+ defer file.Close()
+
+ hash := sha256.New()
+ if _, err := io.Copy(hash, file); err != nil {
+ return "", errors.New(fmt.Sprint("Failed to compute sha256 of ", path, ": ", err))
+ }
+ return fmt.Sprintf("%x", hash.Sum(nil)), nil
+}
+
+// Finds the checksums for all the files in the specified directory. This is a
+// best effort only. If for some reason we fail to compute the checksum of
+// something, we just move on.
+func findAllFileShas(directory string) map[string]string {
+ shaSums := make(map[string]string)
+
+ // Find the checksums for all the files.
+ err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ log.Println("Walk() didn't want to deal with ", path, ":", err)
+ return nil
+ }
+ if info.IsDir() {
+ // We only care about computing checksums of files.
+ // Ignore directories.
+ return nil
+ }
+ hash, err := computeSha256(path)
+ if err != nil {
+ log.Println(err)
+ return nil
+ }
+ shaSums[hash] = "/" + strings.TrimPrefix(path, directory)
+ return nil
+ })
+ if err != nil {
+ log.Fatal("Got unexpected error from Walk(): ", err)
+ }
+
+ return shaSums
+}
+
+func HandleShaUrl(directory string, h http.Handler) http.Handler {
+ shaSums := findAllFileShas(directory)
+
+ fn := func(w http.ResponseWriter, r *http.Request) {
+ // We expect the path portion to look like this:
+ // /sha256/<checksum>/path...
+ // Splitting on / means we end up with this list:
+ // [0] ""
+ // [1] "sha256"
+ // [2] "<checksum>"
+ // [3-] path...
+ parts := strings.Split(r.URL.Path, "/")
+ if len(parts) < 4 {
+ w.WriteHeader(http.StatusNotFound)
+ return
+ }
+ if parts[0] != "" || parts[1] != "sha256" {
+ // Something is fundamentally wrong. We told the
+ // framework to only give is /sha256/ requests.
+ log.Fatal("This handler should not be called for " + r.URL.Path)
+ }
+ hash := parts[2]
+ if path, ok := shaSums[hash]; ok {
+ // We found a file with this checksum. Serve that file.
+ r.URL.Path = path
+ } else {
+ // No file with this checksum found.
+ w.WriteHeader(http.StatusNotFound)
+ return
}
h.ServeHTTP(w, r)
@@ -35,8 +131,8 @@
// Serve pages in the specified directory.
func ServePages(scoutingServer server.ScoutingServer, directory string) {
// Serve the / endpoint given a folder of pages.
- scoutingServer.Handle("/", NoCache(http.FileServer(http.Dir(directory))))
- // Make an exception for pictures. We don't want the pictures to be
- // pulled every time the page is refreshed.
- scoutingServer.Handle("/pictures/", http.FileServer(http.Dir(directory)))
+ scoutingServer.Handle("/", MaybeNoCache(http.FileServer(http.Dir(directory))))
+
+ // Also serve files in a checksum-addressable manner.
+ scoutingServer.Handle("/sha256/", HandleShaUrl(directory, http.FileServer(http.Dir(directory))))
}
diff --git a/scouting/webserver/static/static_test.go b/scouting/webserver/static/static_test.go
index 3524c05..09ed940 100644
--- a/scouting/webserver/static/static_test.go
+++ b/scouting/webserver/static/static_test.go
@@ -9,6 +9,12 @@
"github.com/frc971/971-Robot-Code/scouting/webserver/server"
)
+func expectEqual(t *testing.T, actual string, expected string) {
+ if actual != expected {
+ t.Error("Expected ", actual, " to equal ", expected)
+ }
+}
+
func TestServing(t *testing.T) {
cases := []struct {
// The path to request from the server.
@@ -17,6 +23,7 @@
// specified path.
expectedData string
}{
+ {"/", "<h1>This is the index</h1>\n"},
{"/root.txt", "Hello, this is the root page!"},
{"/page.txt", "Hello from a page!"},
}
@@ -24,37 +31,67 @@
scoutingServer := server.NewScoutingServer()
ServePages(scoutingServer, "test_pages")
scoutingServer.Start(8080)
+ defer scoutingServer.Stop()
// Go through all the test cases, and run them against the running webserver.
for _, c := range cases {
dataReceived := getData(c.path, t)
- if dataReceived != c.expectedData {
- t.Errorf("Got %q, but expected %q", dataReceived, c.expectedData)
- }
+ expectEqual(t, dataReceived, c.expectedData)
}
-
- scoutingServer.Stop()
}
-func TestCache(t *testing.T) {
+// Makes sure that requesting / sets the proper headers so it doesn't get
+// cached.
+func TestDisallowedCache(t *testing.T) {
scoutingServer := server.NewScoutingServer()
ServePages(scoutingServer, "test_pages")
scoutingServer.Start(8080)
+ defer scoutingServer.Stop()
+
+ resp, err := http.Get("http://localhost:8080/")
+ if err != nil {
+ t.Fatal("Failed to get data ", err)
+ }
+ expectEqual(t, resp.Header.Get("Expires"), "Thu, 01 Jan 1970 00:00:00 UTC")
+ expectEqual(t, resp.Header.Get("Cache-Control"), "no-cache, private, max-age=0")
+ expectEqual(t, resp.Header.Get("Pragma"), "no-cache")
+ expectEqual(t, resp.Header.Get("X-Accel-Expires"), "0")
+}
+
+// Makes sure that requesting anything other than / doesn't set the "do not
+// cache" headers.
+func TestAllowedCache(t *testing.T) {
+ scoutingServer := server.NewScoutingServer()
+ ServePages(scoutingServer, "test_pages")
+ scoutingServer.Start(8080)
+ defer scoutingServer.Stop()
resp, err := http.Get("http://localhost:8080/root.txt")
if err != nil {
t.Fatalf("Failed to get data ", err)
}
- compareString(resp.Header.Get("Expires"), "Thu, 01 Jan 1970 00:00:00 UTC", t)
- compareString(resp.Header.Get("Cache-Control"), "no-cache, private, max-age=0", t)
- compareString(resp.Header.Get("Pragma"), "no-cache", t)
- compareString(resp.Header.Get("X-Accel-Expires"), "0", t)
+ expectEqual(t, resp.Header.Get("Expires"), "")
+ expectEqual(t, resp.Header.Get("Cache-Control"), "")
+ expectEqual(t, resp.Header.Get("Pragma"), "")
+ expectEqual(t, resp.Header.Get("X-Accel-Expires"), "")
}
-func compareString(actual string, expected string, t *testing.T) {
- if actual != expected {
- t.Errorf("Expected ", actual, " to equal ", expected)
+func TestSha256(t *testing.T) {
+ scoutingServer := server.NewScoutingServer()
+ ServePages(scoutingServer, "test_pages")
+ scoutingServer.Start(8080)
+ defer scoutingServer.Stop()
+
+ // Validate a valid checksum.
+ dataReceived := getData("sha256/553b9b29647a112136986cf93c57b988d1f12dc43d3b774f14a24e58d272dbff/root.txt", t)
+ expectEqual(t, dataReceived, "Hello, this is the root page!")
+
+ // Make a request with an invalid checksum and make sure we get a 404.
+ resp, err := http.Get("http://localhost:8080/sha256/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef/root.txt")
+ if err != nil {
+ t.Fatal("Failed to get data ", err)
}
+ expectEqual(t, resp.Status, "404 Not Found")
}
// Retrieves the data at the specified path. If an error occurs, the test case
@@ -66,7 +103,7 @@
}
// Error out if the return status is anything other than 200 OK.
if resp.Status != "200 OK" {
- t.Fatalf("Received a status code other than 200")
+ t.Fatal("Received a status code other than 200:", resp.Status)
}
// Read the response body.
body, err := ioutil.ReadAll(resp.Body)
diff --git a/scouting/webserver/static/test_pages/index.html b/scouting/webserver/static/test_pages/index.html
new file mode 100644
index 0000000..d769db4
--- /dev/null
+++ b/scouting/webserver/static/test_pages/index.html
@@ -0,0 +1 @@
+<h1>This is the index</h1>