@@ -108,10 +108,34 @@ func New(opts *Options) *Handler {
108
108
panic (fmt .Sprintf ("Failed to parse html files: %v" ,err ))
109
109
}
110
110
111
- binHashCache := newBinHashCache (opts .BinFS ,opts .BinHashes )
112
-
113
111
mux := http .NewServeMux ()
114
- mux .Handle ("/bin/" ,http .StripPrefix ("/bin" ,http .HandlerFunc (func (rw http.ResponseWriter ,r * http.Request ) {
112
+ mux .Handle ("/bin/" ,binHandler (opts .BinFS ,newBinMetadataCache (opts .BinFS ,opts .BinHashes )))
113
+ mux .Handle ("/" ,http .FileServer (
114
+ http .FS (
115
+ // OnlyFiles is a wrapper around the file system that prevents directory
116
+ // listings. Directory listings are not required for the site file system, so we
117
+ // exclude it as a security measure. In practice, this file system comes from our
118
+ // open source code base, but this is considered a best practice for serving
119
+ // static files.
120
+ OnlyFiles (opts .SiteFS ))),
121
+ )
122
+ buildInfoResponse ,err := json .Marshal (opts .BuildInfo )
123
+ if err != nil {
124
+ panic ("failed to marshal build info: " + err .Error ())
125
+ }
126
+ handler .buildInfoJSON = html .EscapeString (string (buildInfoResponse ))
127
+ handler .handler = mux .ServeHTTP
128
+
129
+ handler .installScript ,err = parseInstallScript (opts .SiteFS ,opts .BuildInfo )
130
+ if err != nil {
131
+ opts .Logger .Warn (context .Background (),"could not parse install.sh, it will be unavailable" ,slog .Error (err ))
132
+ }
133
+
134
+ return handler
135
+ }
136
+
137
+ func binHandler (binFS http.FileSystem ,binMetadataCache * binMetadataCache ) http.Handler {
138
+ return http .StripPrefix ("/bin" ,http .HandlerFunc (func (rw http.ResponseWriter ,r * http.Request ) {
115
139
// Convert underscores in the filename to hyphens. We eventually want to
116
140
// change our hyphen-based filenames to underscores, but we need to
117
141
// support both for now.
@@ -122,7 +146,7 @@ func New(opts *Options) *Handler {
122
146
if name == "" || name == "/" {
123
147
// Serve the directory listing. This intentionally allows directory listings to
124
148
// be served. This file system should not contain anything sensitive.
125
- http .FileServer (opts . BinFS ).ServeHTTP (rw ,r )
149
+ http .FileServer (binFS ).ServeHTTP (rw ,r )
126
150
return
127
151
}
128
152
if strings .Contains (name ,"/" ) {
@@ -131,7 +155,8 @@ func New(opts *Options) *Handler {
131
155
http .NotFound (rw ,r )
132
156
return
133
157
}
134
- hash ,err := binHashCache .getHash (name )
158
+
159
+ metadata ,err := binMetadataCache .getMetadata (name )
135
160
if xerrors .Is (err ,os .ErrNotExist ) {
136
161
http .NotFound (rw ,r )
137
162
return
@@ -141,35 +166,26 @@ func New(opts *Options) *Handler {
141
166
return
142
167
}
143
168
144
- // ETag header needs to be quoted.
145
- rw .Header ().Set ("ETag" ,fmt .Sprintf (`%q` ,hash ))
169
+ // http.FileServer will not set Content-Length when performing chunked
170
+ // transport encoding, which is used for large files like our binaries
171
+ // so stream compression can be used.
172
+ //
173
+ // Clients like IDE extensions and the desktop apps can compare the
174
+ // value of this header with the amount of bytes written to disk after
175
+ // decompression to show progress. Without this, they cannot show
176
+ // progress without disabling compression.
177
+ //
178
+ // There isn't really a spec for a length header for the "inner" content
179
+ // size, but some nginx modules use this header.
180
+ rw .Header ().Set ("X-Original-Content-Length" ,fmt .Sprintf ("%d" ,metadata .sizeBytes ))
181
+
182
+ // Get and set ETag header. Must be quoted.
183
+ rw .Header ().Set ("ETag" ,fmt .Sprintf (`%q` ,metadata .sha1Hash ))
146
184
147
185
// http.FileServer will see the ETag header and automatically handle
148
186
// If-Match and If-None-Match headers on the request properly.
149
- http .FileServer (opts .BinFS ).ServeHTTP (rw ,r )
150
- })))
151
- mux .Handle ("/" ,http .FileServer (
152
- http .FS (
153
- // OnlyFiles is a wrapper around the file system that prevents directory
154
- // listings. Directory listings are not required for the site file system, so we
155
- // exclude it as a security measure. In practice, this file system comes from our
156
- // open source code base, but this is considered a best practice for serving
157
- // static files.
158
- OnlyFiles (opts .SiteFS ))),
159
- )
160
- buildInfoResponse ,err := json .Marshal (opts .BuildInfo )
161
- if err != nil {
162
- panic ("failed to marshal build info: " + err .Error ())
163
- }
164
- handler .buildInfoJSON = html .EscapeString (string (buildInfoResponse ))
165
- handler .handler = mux .ServeHTTP
166
-
167
- handler .installScript ,err = parseInstallScript (opts .SiteFS ,opts .BuildInfo )
168
- if err != nil {
169
- opts .Logger .Warn (context .Background (),"could not parse install.sh, it will be unavailable" ,slog .Error (err ))
170
- }
171
-
172
- return handler
187
+ http .FileServer (binFS ).ServeHTTP (rw ,r )
188
+ }))
173
189
}
174
190
175
191
type Handler struct {
@@ -217,7 +233,7 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
217
233
h .handler .ServeHTTP (rw ,r )
218
234
return
219
235
// If requesting assets, serve straight up with caching.
220
- case reqFile == "assets" || strings .HasPrefix (reqFile ,"assets/" ):
236
+ case reqFile == "assets" || strings .HasPrefix (reqFile ,"assets/" )|| strings . HasPrefix ( reqFile , "icon/" ) :
221
237
// It could make sense to cache 404s, but the problem is that during an
222
238
// upgrade a load balancer may route partially to the old server, and that
223
239
// would make new asset paths get cached as 404s and not load even once the
@@ -952,68 +968,95 @@ func RenderStaticErrorPage(rw http.ResponseWriter, r *http.Request, data ErrorPa
952
968
}
953
969
}
954
970
955
- type binHashCache struct {
956
- binFS http.FileSystem
971
+ type binMetadata struct {
972
+ sizeBytes int64 // -1 if not known yet
973
+ // SHA1 was chosen because it's fast to compute and reasonable for
974
+ // determining if a file has changed. The ETag is not used a security
975
+ // measure.
976
+ sha1Hash string // always set if in the cache
977
+ }
978
+
979
+ type binMetadataCache struct {
980
+ binFS http.FileSystem
981
+ originalHashes map [string ]string
957
982
958
- hashes map [string ]string
959
- mut sync.RWMutex
960
- sf singleflight.Group
961
- sem chan struct {}
983
+ metadata map [string ]binMetadata
984
+ mut sync.RWMutex
985
+ sf singleflight.Group
986
+ sem chan struct {}
962
987
}
963
988
964
- func newBinHashCache (binFS http.FileSystem ,binHashes map [string ]string )* binHashCache {
965
- b := & binHashCache {
966
- binFS :binFS ,
967
- hashes :make (map [string ]string ,len (binHashes )),
968
- mut : sync.RWMutex {},
969
- sf : singleflight.Group {},
970
- sem :make (chan struct {},4 ),
989
+ func newBinMetadataCache (binFS http.FileSystem ,binSha1Hashes map [string ]string )* binMetadataCache {
990
+ b := & binMetadataCache {
991
+ binFS :binFS ,
992
+ originalHashes :make (map [string ]string ,len (binSha1Hashes )),
993
+
994
+ metadata :make (map [string ]binMetadata ,len (binSha1Hashes )),
995
+ mut : sync.RWMutex {},
996
+ sf : singleflight.Group {},
997
+ sem :make (chan struct {},4 ),
971
998
}
972
- // Make a copy since we're gonna be mutating it.
973
- for k ,v := range binHashes {
974
- b .hashes [k ]= v
999
+
1000
+ // Previously we copied binSha1Hashes to the cache immediately. Since we now
1001
+ // read other information like size from the file, we can't do that. Instead
1002
+ // we copy the hashes to a different map that will be used to populate the
1003
+ // cache on the first request.
1004
+ for k ,v := range binSha1Hashes {
1005
+ b .originalHashes [k ]= v
975
1006
}
976
1007
977
1008
return b
978
1009
}
979
1010
980
- func (b * binHashCache ) getHash (name string ) (string ,error ) {
1011
+ func (b * binMetadataCache ) getMetadata (name string ) (binMetadata ,error ) {
981
1012
b .mut .RLock ()
982
- hash ,ok := b .hashes [name ]
1013
+ metadata ,ok := b .metadata [name ]
983
1014
b .mut .RUnlock ()
984
1015
if ok {
985
- return hash ,nil
1016
+ return metadata ,nil
986
1017
}
987
1018
988
1019
// Avoid DOS by using a pool, and only doing work once per file.
989
- v ,err ,_ := b .sf .Do (name ,func () (interface {} ,error ) {
1020
+ v ,err ,_ := b .sf .Do (name ,func () (any ,error ) {
990
1021
b .sem <- struct {}{}
991
1022
defer func () {<- b .sem }()
992
1023
993
1024
f ,err := b .binFS .Open (name )
994
1025
if err != nil {
995
- return "" ,err
1026
+ return binMetadata {} ,err
996
1027
}
997
1028
defer f .Close ()
998
1029
999
- h := sha1 .New ()//#nosec // Not used for cryptography.
1000
- _ ,err = io .Copy (h ,f )
1030
+ var metadata binMetadata
1031
+
1032
+ stat ,err := f .Stat ()
1001
1033
if err != nil {
1002
- return "" ,err
1034
+ return binMetadata {},err
1035
+ }
1036
+ metadata .sizeBytes = stat .Size ()
1037
+
1038
+ if hash ,ok := b .originalHashes [name ];ok {
1039
+ metadata .sha1Hash = hash
1040
+ }else {
1041
+ h := sha1 .New ()//#nosec // Not used for cryptography.
1042
+ _ ,err := io .Copy (h ,f )
1043
+ if err != nil {
1044
+ return binMetadata {},err
1045
+ }
1046
+ metadata .sha1Hash = hex .EncodeToString (h .Sum (nil ))
1003
1047
}
1004
1048
1005
- hash := hex .EncodeToString (h .Sum (nil ))
1006
1049
b .mut .Lock ()
1007
- b .hashes [name ]= hash
1050
+ b .metadata [name ]= metadata
1008
1051
b .mut .Unlock ()
1009
- return hash ,nil
1052
+ return metadata ,nil
1010
1053
})
1011
1054
if err != nil {
1012
- return "" ,err
1055
+ return binMetadata {} ,err
1013
1056
}
1014
1057
1015
1058
//nolint:forcetypeassert
1016
- return strings . ToLower ( v .(string ) ),nil
1059
+ return v .(binMetadata ),nil
1017
1060
}
1018
1061
1019
1062
func applicationNameOrDefault (cfg codersdk.AppearanceConfig )string {