1
1
package site
2
2
3
3
import (
4
+ "archive/tar"
4
5
"bytes"
5
6
"context"
6
-
7
+ "errors"
7
8
"fmt"
8
9
"io"
9
10
"io/fs"
10
11
"net/http"
12
+ "os"
11
13
"path"
12
14
"path/filepath"
13
15
"strings"
14
16
"text/template" // html/template escapes some nonces
15
17
"time"
16
18
17
19
"github.com/justinas/nosurf"
20
+ "github.com/klauspost/compress/zstd"
18
21
"github.com/unrolled/secure"
22
+ "golang.org/x/exp/slices"
19
23
"golang.org/x/xerrors"
20
24
)
21
25
@@ -29,22 +33,25 @@ func WithAPIResponse(ctx context.Context, apiResponse APIResponse) context.Conte
29
33
}
30
34
31
35
// Handler returns an HTTP handler for serving the static site.
32
- func Handler (fileSystem fs.FS ) http.Handler {
36
+ func Handler (siteFS fs.FS , binFS http. FileSystem ) http.Handler {
33
37
// html files are handled by a text/template. Non-html files
34
38
// are served by the default file server.
35
39
//
36
40
// REMARK: text/template is needed to inject values on each request like
37
41
// CSRF.
38
- files , err := htmlFiles (fileSystem )
39
-
42
+ files , err := htmlFiles (siteFS )
40
43
if err != nil {
41
44
panic (xerrors .Errorf ("Failed to return handler for static files. Html files failed to load: %w" , err ))
42
45
}
43
46
47
+ mux := http .NewServeMux ()
48
+ mux .Handle ("/bin/" , http .StripPrefix ("/bin" , http .FileServer (binFS )))
49
+ mux .Handle ("/" , http .FileServer (http .FS (siteFS ))) // All other non-html static files.
50
+
44
51
return secureHeaders (& handler {
45
- fs : fileSystem ,
52
+ fs : siteFS ,
46
53
htmlFiles : files ,
47
- h : http . FileServer ( http . FS ( fileSystem )), // All other non-html static files
54
+ h : mux ,
48
55
})
49
56
}
50
57
@@ -146,8 +153,13 @@ func (h *handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
146
153
return
147
154
}
148
155
156
+ switch {
157
+ // If requesting binaries, serve straight up.
158
+ case reqFile == "bin" || strings .HasPrefix (reqFile , "bin/" ):
159
+ h .h .ServeHTTP (resp , req )
160
+ return
149
161
// If the original file path exists we serve it.
150
- if h .exists (reqFile ) {
162
+ case h .exists (reqFile ):
151
163
if ShouldCacheFile (reqFile ) {
152
164
resp .Header ().Add ("Cache-Control" , "public, max-age=31536000, immutable" )
153
165
}
@@ -357,7 +369,6 @@ func htmlFiles(files fs.FS) (*htmlTemplates, error) {
357
369
358
370
return nil
359
371
})
360
-
361
372
if err != nil {
362
373
return nil , err
363
374
}
@@ -366,3 +377,135 @@ func htmlFiles(files fs.FS) (*htmlTemplates, error) {
366
377
tpls : root ,
367
378
}, nil
368
379
}
380
+
381
+ // ExtractOrReadBinFS checks the provided fs for compressed coder
382
+ // binaries and extracts them into dest/bin if found. As a fallback,
383
+ // the provided FS is checked for a /bin directory, if it is non-empty
384
+ // it is returned. Finally dest/bin is returned as a fallback allowing
385
+ // binaries to be manually placed in dest (usually
386
+ // ${CODER_CACHE_DIRECTORY}/site/bin).
387
+ func ExtractOrReadBinFS (dest string , siteFS fs.FS ) (http.FileSystem , error ) {
388
+ if dest == "" {
389
+ // No destination on fs, embedded fs is the only option.
390
+ binFS , err := fs .Sub (siteFS , "bin" )
391
+ if err != nil {
392
+ return nil , xerrors .Errorf ("cache path is empty and embedded fs does not have /bin: %w" , err )
393
+ }
394
+ return http .FS (binFS ), nil
395
+ }
396
+
397
+ dest = filepath .Join (dest , "bin" )
398
+ mkdest := func () (http.FileSystem , error ) {
399
+ err := os .MkdirAll (dest , 0o700 )
400
+ if err != nil {
401
+ return nil , xerrors .Errorf ("mkdir failed: %w" , err )
402
+ }
403
+ return http .Dir (dest ), nil
404
+ }
405
+
406
+ archive , err := siteFS .Open ("bin/coder.tar.zst" )
407
+ if err != nil {
408
+ if xerrors .Is (err , fs .ErrNotExist ) {
409
+ files , err := fs .ReadDir (siteFS , "bin" )
410
+ if err != nil {
411
+ if xerrors .Is (err , fs .ErrNotExist ) {
412
+ // Given fs does not have a bin directory,
413
+ // serve from cache directory.
414
+ return mkdest ()
415
+ }
416
+ return nil , xerrors .Errorf ("site fs read dir failed: %w" , err )
417
+ }
418
+
419
+ if len (filterFiles (files , "GITKEEP" )) > 0 {
420
+ // If there are other files than bin/GITKEEP,
421
+ // serve the files.
422
+ binFS , err := fs .Sub (siteFS , "bin" )
423
+ if err != nil {
424
+ return nil , xerrors .Errorf ("site fs sub dir failed: %w" , err )
425
+ }
426
+ return http .FS (binFS ), nil
427
+ }
428
+
429
+ // Nothing we can do, serve the cache directory,
430
+ // thus allowing binaries to be places there.
431
+ return mkdest ()
432
+ }
433
+ return nil , xerrors .Errorf ("open coder binary archive failed: %w" , err )
434
+ }
435
+ defer archive .Close ()
436
+
437
+ dir , err := mkdest ()
438
+ if err != nil {
439
+ return nil , err
440
+ }
441
+
442
+ n , err := extractBin (dest , archive )
443
+ if err != nil {
444
+ return nil , xerrors .Errorf ("extract coder binaries failed: %w" , err )
445
+ }
446
+ if n == 0 {
447
+ return nil , xerrors .New ("no files were extracted from coder binaries archive" )
448
+ }
449
+
450
+ return dir , nil
451
+ }
452
+
453
+ func filterFiles (files []fs.DirEntry , names ... string ) []fs.DirEntry {
454
+ var filtered []fs.DirEntry
455
+ for _ , f := range files {
456
+ if slices .Contains (names , f .Name ()) {
457
+ continue
458
+ }
459
+ filtered = append (filtered , f )
460
+ }
461
+ return filtered
462
+ }
463
+
464
+ func extractBin (dest string , r io.Reader ) (numExtraced int , err error ) {
465
+ opts := []zstd.DOption {
466
+ // Concurrency doesn't help us when decoding the tar and
467
+ // can actually slow us down.
468
+ zstd .WithDecoderConcurrency (1 ),
469
+ // Ignoring checksums can give a slight performance
470
+ // boost but it's probalby not worth the reduced safety.
471
+ zstd .IgnoreChecksum (false ),
472
+ // Allow the decoder to use more memory giving us a 2-3x
473
+ // performance boost.
474
+ zstd .WithDecoderLowmem (false ),
475
+ }
476
+ zr , err := zstd .NewReader (r , opts ... )
477
+ if err != nil {
478
+ return 0 , xerrors .Errorf ("open zstd archive failed: %w" , err )
479
+ }
480
+ defer zr .Close ()
481
+
482
+ tr := tar .NewReader (zr )
483
+ n := 0
484
+ for {
485
+ h , err := tr .Next ()
486
+ if err != nil {
487
+ if errors .Is (err , io .EOF ) {
488
+ return n , nil
489
+ }
490
+ return n , xerrors .Errorf ("read tar archive failed: %w" , err )
491
+ }
492
+
493
+ name := filepath .Join (dest , filepath .Base (h .Name ))
494
+ f , err := os .Create (name )
495
+ if err != nil {
496
+ return n , xerrors .Errorf ("create file failed: %w" , err )
497
+ }
498
+ //#nosec // We created this tar, no risk of decompression bomb.
499
+ _ , err = io .Copy (f , tr )
500
+ if err != nil {
501
+ _ = f .Close ()
502
+ return n , xerrors .Errorf ("write file contents failed: %w" , err )
503
+ }
504
+ err = f .Close ()
505
+ if err != nil {
506
+ return n , xerrors .Errorf ("close file failed: %w" , err )
507
+ }
508
+
509
+ n ++
510
+ }
511
+ }
0 commit comments