@@ -16,6 +16,7 @@ import (
16
16
"path"
17
17
"path/filepath"
18
18
"strings"
19
+ "sync"
19
20
"text/template" // html/template escapes some nonces
20
21
"time"
21
22
@@ -24,6 +25,7 @@ import (
24
25
"github.com/unrolled/secure"
25
26
"golang.org/x/exp/slices"
26
27
"golang.org/x/sync/errgroup"
28
+ "golang.org/x/sync/singleflight"
27
29
"golang.org/x/xerrors"
28
30
29
31
"github.com/coder/coder/coderd/httpapi"
@@ -48,7 +50,7 @@ func init() {
48
50
}
49
51
50
52
// Handler returns an HTTP handler for serving the static site.
51
- func Handler (siteFS fs.FS ,binFS http.FileSystem ) http.Handler {
53
+ func Handler (siteFS fs.FS ,binFS http.FileSystem , binHashes map [ string ] string ) http.Handler {
52
54
// html files are handled by a text/template. Non-html files
53
55
// are served by the default file server.
54
56
//
@@ -59,13 +61,43 @@ func Handler(siteFS fs.FS, binFS http.FileSystem) http.Handler {
59
61
panic (xerrors .Errorf ("Failed to return handler for static files. Html files failed to load: %w" ,err ))
60
62
}
61
63
64
+ binHashCache := newBinHashCache (binFS ,binHashes )
65
+
62
66
mux := http .NewServeMux ()
63
67
mux .Handle ("/bin/" ,http .StripPrefix ("/bin" ,http .HandlerFunc (func (rw http.ResponseWriter ,r * http.Request ) {
64
68
// Convert underscores in the filename to hyphens. We eventually want to
65
69
// change our hyphen-based filenames to underscores, but we need to
66
70
// support both for now.
67
71
r .URL .Path = strings .ReplaceAll (r .URL .Path ,"_" ,"-" )
68
72
73
+ // Set ETag header to the SHA1 hash of the file contents.
74
+ name := filePath (r .URL .Path )
75
+ if name == "" || name == "/" {
76
+ // Serve the directory listing.
77
+ http .FileServer (binFS ).ServeHTTP (rw ,r )
78
+ return
79
+ }
80
+ if strings .Contains (name ,"/" ) {
81
+ // We only serve files from the root of this directory, so avoid any
82
+ // shenanigans by blocking slashes in the URL path.
83
+ http .NotFound (rw ,r )
84
+ return
85
+ }
86
+ hash ,err := binHashCache .getHash (name )
87
+ if xerrors .Is (err ,os .ErrNotExist ) {
88
+ http .NotFound (rw ,r )
89
+ return
90
+ }
91
+ if err != nil {
92
+ http .Error (rw ,err .Error (),http .StatusInternalServerError )
93
+ return
94
+ }
95
+
96
+ // ETag header needs to be quoted.
97
+ rw .Header ().Set ("ETag" ,fmt .Sprintf (`%q` ,hash ))
98
+
99
+ // http.FileServer will see the ETag header and automatically handle
100
+ // If-Match and If-None-Match headers on the request properly.
69
101
http .FileServer (binFS ).ServeHTTP (rw ,r )
70
102
})))
71
103
mux .Handle ("/" ,http .FileServer (http .FS (siteFS )))// All other non-html static files.
@@ -409,20 +441,23 @@ func htmlFiles(files fs.FS) (*htmlTemplates, error) {
409
441
},nil
410
442
}
411
443
412
- // ExtractOrReadBinFS checks the provided fs for compressed coder
413
- // binaries and extracts them into dest/bin if found. As a fallback,
414
- // the provided FS is checked for a /bin directory, if it is non-empty
415
- // it is returned. Finally dest/bin is returned as a fallback allowing
416
- // binaries to be manually placed in dest (usually
417
- // ${CODER_CACHE_DIRECTORY}/site/bin).
418
- func ExtractOrReadBinFS (dest string ,siteFS fs.FS ) (http.FileSystem ,error ) {
444
+ // ExtractOrReadBinFS checks the provided fs for compressed coder binaries and
445
+ // extracts them into dest/bin if found. As a fallback, the provided FS is
446
+ // checked for a /bin directory, if it is non-empty it is returned. Finally
447
+ // dest/bin is returned as a fallback allowing binaries to be manually placed in
448
+ // dest (usually ${CODER_CACHE_DIRECTORY}/site/bin).
449
+ //
450
+ // Returns a http.FileSystem that serves unpacked binaries, and a map of binary
451
+ // name to SHA1 hash. The returned hash map may be incomplete or contain hashes
452
+ // for missing files.
453
+ func ExtractOrReadBinFS (dest string ,siteFS fs.FS ) (http.FileSystem ,map [string ]string ,error ) {
419
454
if dest == "" {
420
455
// No destination on fs, embedded fs is the only option.
421
456
binFS ,err := fs .Sub (siteFS ,"bin" )
422
457
if err != nil {
423
- return nil ,xerrors .Errorf ("cache path is empty and embedded fs does not have /bin: %w" ,err )
458
+ return nil ,nil , xerrors .Errorf ("cache path is empty and embedded fs does not have /bin: %w" ,err )
424
459
}
425
- return http .FS (binFS ),nil
460
+ return http .FS (binFS ),nil , nil
426
461
}
427
462
428
463
dest = filepath .Join (dest ,"bin" )
@@ -440,51 +475,63 @@ func ExtractOrReadBinFS(dest string, siteFS fs.FS) (http.FileSystem, error) {
440
475
files ,err := fs .ReadDir (siteFS ,"bin" )
441
476
if err != nil {
442
477
if xerrors .Is (err ,fs .ErrNotExist ) {
443
- // Given fs does not have a bin directory,
444
- // serve from cache directory.
445
- return mkdest ()
478
+ // Given fs does not have a bin directory, serve from cache
479
+ // directory without extracting anything.
480
+ binFS ,err := mkdest ()
481
+ if err != nil {
482
+ return nil ,nil ,xerrors .Errorf ("mkdest failed: %w" ,err )
483
+ }
484
+ return binFS ,map [string ]string {},nil
446
485
}
447
- return nil ,xerrors .Errorf ("site fs read dir failed: %w" ,err )
486
+ return nil ,nil , xerrors .Errorf ("site fs read dir failed: %w" ,err )
448
487
}
449
488
450
489
if len (filterFiles (files ,"GITKEEP" ))> 0 {
451
- // If there are other files than bin/GITKEEP,
452
- // serve the files.
490
+ // If there are other files than bin/GITKEEP, serve the files.
453
491
binFS ,err := fs .Sub (siteFS ,"bin" )
454
492
if err != nil {
455
- return nil ,xerrors .Errorf ("site fs sub dir failed: %w" ,err )
493
+ return nil ,nil , xerrors .Errorf ("site fs sub dir failed: %w" ,err )
456
494
}
457
- return http .FS (binFS ),nil
495
+ return http .FS (binFS ),nil , nil
458
496
}
459
497
460
- // Nothing we can do, serve the cache directory,
461
- // thus allowing binaries to be places there.
462
- return mkdest ()
498
+ // Nothing we can do, serve the cache directory, thus allowing
499
+ // binaries to be placed there.
500
+ binFS ,err := mkdest ()
501
+ if err != nil {
502
+ return nil ,nil ,xerrors .Errorf ("mkdest failed: %w" ,err )
503
+ }
504
+ return binFS ,map [string ]string {},nil
463
505
}
464
- return nil ,xerrors .Errorf ("open coder binary archive failed: %w" ,err )
506
+ return nil ,nil , xerrors .Errorf ("open coder binary archive failed: %w" ,err )
465
507
}
466
508
defer archive .Close ()
467
509
468
- dir ,err := mkdest ()
510
+ binFS ,err := mkdest ()
469
511
if err != nil {
470
- return nil ,err
512
+ return nil ,nil ,err
513
+ }
514
+
515
+ shaFiles ,err := parseSHA1 (siteFS )
516
+ if err != nil {
517
+ return nil ,nil ,xerrors .Errorf ("parse sha1 file failed: %w" ,err )
471
518
}
472
519
473
- ok ,err := verifyBinSha1IsCurrent (dest ,siteFS )
520
+ ok ,err := verifyBinSha1IsCurrent (dest ,siteFS , shaFiles )
474
521
if err != nil {
475
- return nil ,xerrors .Errorf ("verify coder binaries sha1 failed: %w" ,err )
522
+ return nil ,nil , xerrors .Errorf ("verify coder binaries sha1 failed: %w" ,err )
476
523
}
477
524
if ! ok {
478
525
n ,err := extractBin (dest ,archive )
479
526
if err != nil {
480
- return nil ,xerrors .Errorf ("extract coder binaries failed: %w" ,err )
527
+ return nil ,nil , xerrors .Errorf ("extract coder binaries failed: %w" ,err )
481
528
}
482
529
if n == 0 {
483
- return nil ,xerrors .New ("no files were extracted from coder binaries archive" )
530
+ return nil ,nil , xerrors .New ("no files were extracted from coder binaries archive" )
484
531
}
485
532
}
486
533
487
- return dir ,nil
534
+ return binFS , shaFiles ,nil
488
535
}
489
536
490
537
func filterFiles (files []fs.DirEntry ,names ... string ) []fs.DirEntry {
@@ -501,24 +548,32 @@ func filterFiles(files []fs.DirEntry, names ...string) []fs.DirEntry {
501
548
// errHashMismatch is a sentinel error used in verifyBinSha1IsCurrent.
502
549
var errHashMismatch = xerrors .New ("hash mismatch" )
503
550
504
- func verifyBinSha1IsCurrent ( dest string , siteFS fs.FS ) (ok bool , err error ) {
505
- b1 ,err := fs .ReadFile (siteFS ,"bin/coder.sha1" )
551
+ func parseSHA1 ( siteFS fs.FS ) (map [ string ] string , error ) {
552
+ b ,err := fs .ReadFile (siteFS ,"bin/coder.sha1" )
506
553
if err != nil {
507
- return false ,xerrors .Errorf ("read coder sha1 from embedded fs failed: %w" ,err )
554
+ return nil ,xerrors .Errorf ("read coder sha1 from embedded fs failed: %w" ,err )
508
555
}
509
- // Parse sha1 file.
510
- shaFiles := make (map [string ][] byte )
511
- for _ ,line := range bytes .Split (bytes .TrimSpace (b1 ), []byte {'\n' }) {
556
+
557
+ shaFiles := make (map [string ]string )
558
+ for _ ,line := range bytes .Split (bytes .TrimSpace (b ), []byte {'\n' }) {
512
559
parts := bytes .Split (line , []byte {' ' ,'*' })
513
560
if len (parts )!= 2 {
514
- return false ,xerrors .Errorf ("malformed sha1 file: %w" ,err )
561
+ return nil ,xerrors .Errorf ("malformed sha1 file: %w" ,err )
515
562
}
516
- shaFiles [string (parts [1 ])]= parts [0 ]
563
+ shaFiles [string (parts [1 ])]= strings . ToLower ( string ( parts [0 ]))
517
564
}
518
565
if len (shaFiles )== 0 {
519
- return false ,xerrors .Errorf ("empty sha1 file: %w" ,err )
566
+ return nil ,xerrors .Errorf ("empty sha1 file: %w" ,err )
520
567
}
521
568
569
+ return shaFiles ,nil
570
+ }
571
+
572
+ func verifyBinSha1IsCurrent (dest string ,siteFS fs.FS ,shaFiles map [string ]string ) (ok bool ,err error ) {
573
+ b1 ,err := fs .ReadFile (siteFS ,"bin/coder.sha1" )
574
+ if err != nil {
575
+ return false ,xerrors .Errorf ("read coder sha1 from embedded fs failed: %w" ,err )
576
+ }
522
577
b2 ,err := os .ReadFile (filepath .Join (dest ,"coder.sha1" ))
523
578
if err != nil {
524
579
if xerrors .Is (err ,fs .ErrNotExist ) {
@@ -551,7 +606,7 @@ func verifyBinSha1IsCurrent(dest string, siteFS fs.FS) (ok bool, err error) {
551
606
}
552
607
return xerrors .Errorf ("hash file failed: %w" ,err )
553
608
}
554
- if ! bytes . Equal (hash1 ,hash2 ) {
609
+ if ! strings . EqualFold (hash1 ,hash2 ) {
555
610
return errHashMismatch
556
611
}
557
612
return nil
@@ -570,24 +625,24 @@ func verifyBinSha1IsCurrent(dest string, siteFS fs.FS) (ok bool, err error) {
570
625
571
626
// sha1HashFile computes a SHA1 hash of the file, returning the hex
572
627
// representation.
573
- func sha1HashFile (name string ) ([] byte ,error ) {
628
+ func sha1HashFile (name string ) (string ,error ) {
574
629
//#nosec // Not used for cryptography.
575
630
hash := sha1 .New ()
576
631
f ,err := os .Open (name )
577
632
if err != nil {
578
- return nil ,err
633
+ return "" ,err
579
634
}
580
635
defer f .Close ()
581
636
582
637
_ ,err = io .Copy (hash ,f )
583
638
if err != nil {
584
- return nil ,err
639
+ return "" ,err
585
640
}
586
641
587
642
b := make ([]byte ,hash .Size ())
588
643
hash .Sum (b [:0 ])
589
644
590
- return [] byte ( hex .EncodeToString (b ) ),nil
645
+ return hex .EncodeToString (b ),nil
591
646
}
592
647
593
648
func extractBin (dest string ,r io.Reader ) (numExtracted int ,err error ) {
@@ -672,3 +727,67 @@ func RenderStaticErrorPage(rw http.ResponseWriter, r *http.Request, data ErrorPa
672
727
return
673
728
}
674
729
}
730
+
731
+ type binHashCache struct {
732
+ binFS http.FileSystem
733
+
734
+ hashes map [string ]string
735
+ mut sync.RWMutex
736
+ sf singleflight.Group
737
+ sem chan struct {}
738
+ }
739
+
740
+ func newBinHashCache (binFS http.FileSystem ,binHashes map [string ]string )* binHashCache {
741
+ b := & binHashCache {
742
+ binFS :binFS ,
743
+ hashes :make (map [string ]string ,len (binHashes )),
744
+ mut : sync.RWMutex {},
745
+ sf : singleflight.Group {},
746
+ sem :make (chan struct {},4 ),
747
+ }
748
+ // Make a copy since we're gonna be mutating it.
749
+ for k ,v := range binHashes {
750
+ b .hashes [k ]= v
751
+ }
752
+
753
+ return b
754
+ }
755
+
756
+ func (b * binHashCache )getHash (name string ) (string ,error ) {
757
+ b .mut .RLock ()
758
+ hash ,ok := b .hashes [name ]
759
+ b .mut .RUnlock ()
760
+ if ok {
761
+ return hash ,nil
762
+ }
763
+
764
+ // Avoid DOS by using a pool, and only doing work once per file.
765
+ v ,err ,_ := b .sf .Do (name ,func () (interface {},error ) {
766
+ b .sem <- struct {}{}
767
+ defer func () {<- b .sem }()
768
+
769
+ f ,err := b .binFS .Open (name )
770
+ if err != nil {
771
+ return "" ,err
772
+ }
773
+ defer f .Close ()
774
+
775
+ h := sha1 .New ()//#nosec // Not used for cryptography.
776
+ _ ,err = io .Copy (h ,f )
777
+ if err != nil {
778
+ return "" ,err
779
+ }
780
+
781
+ hash := hex .EncodeToString (h .Sum (nil ))
782
+ b .mut .Lock ()
783
+ b .hashes [name ]= hash
784
+ b .mut .Unlock ()
785
+ return hash ,nil
786
+ })
787
+ if err != nil {
788
+ return "" ,err
789
+ }
790
+
791
+ //nolint:forcetypeassert
792
+ return strings .ToLower (v .(string )),nil
793
+ }