Philipp Schrader | 5562df7 | 2022-02-16 20:56:51 -0800 | [diff] [blame] | 1 | package static |
| 2 | |
| 3 | // A year agnostic way to serve static http files. |
| 4 | import ( |
Philipp Schrader | 45721a7 | 2022-04-02 16:27:53 -0700 | [diff] [blame] | 5 | "crypto/sha256" |
| 6 | "errors" |
| 7 | "fmt" |
| 8 | "io" |
| 9 | "log" |
Emily Markova | faecfe1 | 2023-07-01 12:40:03 -0700 | [diff] [blame] | 10 | "mime" |
Philipp Schrader | 5562df7 | 2022-02-16 20:56:51 -0800 | [diff] [blame] | 11 | "net/http" |
Philipp Schrader | 45721a7 | 2022-04-02 16:27:53 -0700 | [diff] [blame] | 12 | "os" |
| 13 | "path/filepath" |
| 14 | "strings" |
Alex Perry | b2f7652 | 2022-03-30 21:02:05 -0700 | [diff] [blame] | 15 | "time" |
Philipp Schrader | 5562df7 | 2022-02-16 20:56:51 -0800 | [diff] [blame] | 16 | |
Emily Markova | faecfe1 | 2023-07-01 12:40:03 -0700 | [diff] [blame] | 17 | "github.com/frc971/971-Robot-Code/scouting/db" |
Philipp Schrader | 5562df7 | 2022-02-16 20:56:51 -0800 | [diff] [blame] | 18 | "github.com/frc971/971-Robot-Code/scouting/webserver/server" |
| 19 | ) |
| 20 | |
Alex Perry | b2f7652 | 2022-03-30 21:02:05 -0700 | [diff] [blame] | 21 | // 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. |
| 24 | var epoch = time.Unix(0, 0).Format(time.RFC1123) |
| 25 | |
| 26 | var 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 Markova | faecfe1 | 2023-07-01 12:40:03 -0700 | [diff] [blame] | 33 | type ScoutingDatabase interface { |
| 34 | QueryPitImageByChecksum(checksum string) (db.PitImage, error) |
| 35 | } |
| 36 | |
Philipp Schrader | 45721a7 | 2022-04-02 16:27:53 -0700 | [diff] [blame] | 37 | func MaybeNoCache(h http.Handler) http.Handler { |
Alex Perry | b2f7652 | 2022-03-30 21:02:05 -0700 | [diff] [blame] | 38 | fn := func(w http.ResponseWriter, r *http.Request) { |
Philipp Schrader | 45721a7 | 2022-04-02 16:27:53 -0700 | [diff] [blame] | 39 | // 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. |
| 54 | func 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. |
| 71 | func 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 Schrader | 67fe6d0 | 2022-04-16 15:37:40 -0700 | [diff] [blame] | 90 | // 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 Schrader | 45721a7 | 2022-04-02 16:27:53 -0700 | [diff] [blame] | 98 | return nil |
| 99 | }) |
| 100 | if err != nil { |
| 101 | log.Fatal("Got unexpected error from Walk(): ", err) |
| 102 | } |
| 103 | |
| 104 | return shaSums |
| 105 | } |
| 106 | |
Emily Markova | faecfe1 | 2023-07-01 12:40:03 -0700 | [diff] [blame] | 107 | func HandleShaUrl(directory string, h http.Handler, scoutingDatabase ScoutingDatabase) http.Handler { |
Philipp Schrader | 45721a7 | 2022-04-02 16:27:53 -0700 | [diff] [blame] | 108 | 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 Schrader | 67fe6d0 | 2022-04-16 15:37:40 -0700 | [diff] [blame] | 117 | // [3] path... |
| 118 | parts := strings.SplitN(r.URL.Path, "/", 4) |
| 119 | if len(parts) != 4 { |
Philipp Schrader | 45721a7 | 2022-04-02 16:27:53 -0700 | [diff] [blame] | 120 | 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 Schrader | 67fe6d0 | 2022-04-16 15:37:40 -0700 | [diff] [blame] | 130 | // 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 Schrader | 45721a7 | 2022-04-02 16:27:53 -0700 | [diff] [blame] | 138 | // We found a file with this checksum. Serve that file. |
| 139 | r.URL.Path = path |
| 140 | } else { |
Emily Markova | faecfe1 | 2023-07-01 12:40:03 -0700 | [diff] [blame] | 141 | 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 Perry | b2f7652 | 2022-03-30 21:02:05 -0700 | [diff] [blame] | 155 | } |
| 156 | |
| 157 | h.ServeHTTP(w, r) |
| 158 | } |
| 159 | |
| 160 | return http.HandlerFunc(fn) |
| 161 | } |
| 162 | |
Emily Markova | faecfe1 | 2023-07-01 12:40:03 -0700 | [diff] [blame] | 163 | func ServePages(scoutingServer server.ScoutingServer, directory string, scoutingDatabase ScoutingDatabase) { |
Philipp Schrader | 5562df7 | 2022-02-16 20:56:51 -0800 | [diff] [blame] | 164 | // Serve the / endpoint given a folder of pages. |
Philipp Schrader | 45721a7 | 2022-04-02 16:27:53 -0700 | [diff] [blame] | 165 | scoutingServer.Handle("/", MaybeNoCache(http.FileServer(http.Dir(directory)))) |
| 166 | |
| 167 | // Also serve files in a checksum-addressable manner. |
Emily Markova | faecfe1 | 2023-07-01 12:40:03 -0700 | [diff] [blame] | 168 | scoutingServer.Handle("/sha256/", HandleShaUrl(directory, http.FileServer(http.Dir(directory)), scoutingDatabase)) |
Philipp Schrader | 5562df7 | 2022-02-16 20:56:51 -0800 | [diff] [blame] | 169 | } |