blob: 8e2e4006c1cc11ae322c54c23dd58059610e9680 [file] [log] [blame]
Philipp Schrader5562df72022-02-16 20:56:51 -08001package static
2
3// A year agnostic way to serve static http files.
4import (
Philipp Schrader45721a72022-04-02 16:27:53 -07005 "crypto/sha256"
6 "errors"
7 "fmt"
8 "io"
9 "log"
Emily Markovafaecfe12023-07-01 12:40:03 -070010 "mime"
Philipp Schrader5562df72022-02-16 20:56:51 -080011 "net/http"
Philipp Schrader45721a72022-04-02 16:27:53 -070012 "os"
13 "path/filepath"
14 "strings"
Alex Perryb2f76522022-03-30 21:02:05 -070015 "time"
Philipp Schrader5562df72022-02-16 20:56:51 -080016
Emily Markovafaecfe12023-07-01 12:40:03 -070017 "github.com/frc971/971-Robot-Code/scouting/db"
Philipp Schrader5562df72022-02-16 20:56:51 -080018 "github.com/frc971/971-Robot-Code/scouting/webserver/server"
19)
20
Alex Perryb2f76522022-03-30 21:02:05 -070021// We want the static files (which include JS that is modified over time), to not be cached.
22// This ensures users get updated versions when uploaded to the server.
23// Based on https://stackoverflow.com/a/33881296, this disables cache for most browsers.
24var epoch = time.Unix(0, 0).Format(time.RFC1123)
25
26var noCacheHeaders = map[string]string{
27 "Expires": epoch,
28 "Cache-Control": "no-cache, private, max-age=0",
29 "Pragma": "no-cache",
30 "X-Accel-Expires": "0",
31}
32
Emily Markovafaecfe12023-07-01 12:40:03 -070033type ScoutingDatabase interface {
34 QueryPitImageByChecksum(checksum string) (db.PitImage, error)
35}
36
Philipp Schrader45721a72022-04-02 16:27:53 -070037func MaybeNoCache(h http.Handler) http.Handler {
Alex Perryb2f76522022-03-30 21:02:05 -070038 fn := func(w http.ResponseWriter, r *http.Request) {
Philipp Schrader45721a72022-04-02 16:27:53 -070039 // We force the browser not to cache index.html so that
40 // browsers will notice when the bundle gets updated.
41 if r.URL.Path == "/" || r.URL.Path == "/index.html" {
42 for k, v := range noCacheHeaders {
43 w.Header().Set(k, v)
44 }
45 }
46
47 h.ServeHTTP(w, r)
48 }
49
50 return http.HandlerFunc(fn)
51}
52
53// Computes the sha256 of the specified file.
54func computeSha256(path string) (string, error) {
55 file, err := os.Open(path)
56 if err != nil {
57 return "", errors.New(fmt.Sprint("Failed to open ", path, ": ", err))
58 }
59 defer file.Close()
60
61 hash := sha256.New()
62 if _, err := io.Copy(hash, file); err != nil {
63 return "", errors.New(fmt.Sprint("Failed to compute sha256 of ", path, ": ", err))
64 }
65 return fmt.Sprintf("%x", hash.Sum(nil)), nil
66}
67
68// Finds the checksums for all the files in the specified directory. This is a
69// best effort only. If for some reason we fail to compute the checksum of
70// something, we just move on.
71func findAllFileShas(directory string) map[string]string {
72 shaSums := make(map[string]string)
73
74 // Find the checksums for all the files.
75 err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
76 if err != nil {
77 log.Println("Walk() didn't want to deal with ", path, ":", err)
78 return nil
79 }
80 if info.IsDir() {
81 // We only care about computing checksums of files.
82 // Ignore directories.
83 return nil
84 }
85 hash, err := computeSha256(path)
86 if err != nil {
87 log.Println(err)
88 return nil
89 }
Philipp Schrader67fe6d02022-04-16 15:37:40 -070090 // We want all paths relative to the original search directory.
91 // That means we remove the search directory from the Walk()
92 // result. Also make sure that the final path doesn't start
93 // with a "/" to make it independent of whether "directory"
94 // ends with a "/" or not.
95 trimmedPath := strings.TrimPrefix(path, directory)
96 trimmedPath = strings.TrimPrefix(trimmedPath, "/")
97 shaSums[hash] = trimmedPath
Philipp Schrader45721a72022-04-02 16:27:53 -070098 return nil
99 })
100 if err != nil {
101 log.Fatal("Got unexpected error from Walk(): ", err)
102 }
103
104 return shaSums
105}
106
Emily Markovafaecfe12023-07-01 12:40:03 -0700107func HandleShaUrl(directory string, h http.Handler, scoutingDatabase ScoutingDatabase) http.Handler {
Philipp Schrader45721a72022-04-02 16:27:53 -0700108 shaSums := findAllFileShas(directory)
109
110 fn := func(w http.ResponseWriter, r *http.Request) {
111 // We expect the path portion to look like this:
112 // /sha256/<checksum>/path...
113 // Splitting on / means we end up with this list:
114 // [0] ""
115 // [1] "sha256"
116 // [2] "<checksum>"
Philipp Schrader67fe6d02022-04-16 15:37:40 -0700117 // [3] path...
118 parts := strings.SplitN(r.URL.Path, "/", 4)
119 if len(parts) != 4 {
Philipp Schrader45721a72022-04-02 16:27:53 -0700120 w.WriteHeader(http.StatusNotFound)
121 return
122 }
123 if parts[0] != "" || parts[1] != "sha256" {
124 // Something is fundamentally wrong. We told the
125 // framework to only give is /sha256/ requests.
126 log.Fatal("This handler should not be called for " + r.URL.Path)
127 }
128 hash := parts[2]
129 if path, ok := shaSums[hash]; ok {
Philipp Schrader67fe6d02022-04-16 15:37:40 -0700130 // The path must match what it would be without the
131 // /sha256/<checksum>/ prefix. Otherwise it's too easy
132 // to make copy-paste mistakes.
133 if path != parts[3] {
134 log.Println("Got ", parts[3], "expected", path)
135 w.WriteHeader(http.StatusBadRequest)
136 return
137 }
Philipp Schrader45721a72022-04-02 16:27:53 -0700138 // We found a file with this checksum. Serve that file.
139 r.URL.Path = path
140 } else {
Emily Markovafaecfe12023-07-01 12:40:03 -0700141 pitImage, err := scoutingDatabase.QueryPitImageByChecksum(hash)
142 if err == nil {
143 if parts[3] != pitImage.ImagePath {
144 log.Println("Got ", parts[3], "expected", pitImage.ImagePath)
145 w.WriteHeader(http.StatusBadRequest)
146 return
147 }
148 w.Header().Add("Content-Type", mime.TypeByExtension(pitImage.ImagePath))
149 w.Write(pitImage.ImageData)
150 return
151 } else { // No file with this checksum found.
152 w.WriteHeader(http.StatusNotFound)
153 return
154 }
Alex Perryb2f76522022-03-30 21:02:05 -0700155 }
156
157 h.ServeHTTP(w, r)
158 }
159
160 return http.HandlerFunc(fn)
161}
162
Emily Markovafaecfe12023-07-01 12:40:03 -0700163func ServePages(scoutingServer server.ScoutingServer, directory string, scoutingDatabase ScoutingDatabase) {
Philipp Schrader5562df72022-02-16 20:56:51 -0800164 // Serve the / endpoint given a folder of pages.
Philipp Schrader45721a72022-04-02 16:27:53 -0700165 scoutingServer.Handle("/", MaybeNoCache(http.FileServer(http.Dir(directory))))
166
167 // Also serve files in a checksum-addressable manner.
Emily Markovafaecfe12023-07-01 12:40:03 -0700168 scoutingServer.Handle("/sha256/", HandleShaUrl(directory, http.FileServer(http.Dir(directory)), scoutingDatabase))
Philipp Schrader5562df72022-02-16 20:56:51 -0800169}