18
"bytes"
19
"compress/gzip"
20
"context"
21
+ "encoding/json"
22
"errors"
23
"fmt"
24
"net/http"
446
}
447
}
448
}
449
+
450
+// A trailing slash denotes a directory, so path matchers don't treat
451
+// "/secret.env/" as "*.env"; the file server must not serve the file either.
452
+func TestServeHTTPTrailingSlashOnFile(t *testing.T) {
453
+ root := t.TempDir()
454
+ if err := os.WriteFile(filepath.Join(root, "secret.env"), []byte("secret"), 0o600); err != nil {
455
+ t.Fatal(err)
456
+ }
457
+ if err := os.WriteFile(filepath.Join(root, "secret.env.gz"), gzipBytes(t, []byte("secret")), 0o600); err != nil {
458
+ t.Fatal(err)
459
+ }
460
+ if err := os.Mkdir(filepath.Join(root, "dir"), 0o700); err != nil {
461
+ t.Fatal(err)
462
+ }
463
+ if err := os.WriteFile(filepath.Join(root, "dir", "index.html"), []byte("index"), 0o600); err != nil {
464
+ t.Fatal(err)
465
+ }
466
+
467
+ for i, tc := range []struct {
468
+ path string
469
+ canonical bool
470
+ acceptEncoding string
471
+ passThru bool
472
+ wantStatus int
473
+ wantLocation string
474
+ wantNext bool
475
+ }{
476
+ {path: "/secret.env", wantStatus: http.StatusOK},
477
+ {path: "/secret.env/", wantStatus: http.StatusNotFound},
478
+ {path: "/secret.env/./", wantStatus: http.StatusNotFound},
479
+ {path: "/secret.env//", wantStatus: http.StatusNotFound},
480
+ {path: "/secret.env%2F", wantStatus: http.StatusNotFound},
481
+ {path: "/secret.env%5c", wantStatus: http.StatusNotFound},
482
+ {path: "/secret.env%5C", wantStatus: http.StatusNotFound},
483
+ {path: `/secret.env\`, wantStatus: http.StatusNotFound},
484
+ {path: "/secret.env%5c", canonical: true, wantStatus: http.StatusNotFound},
485
+ {path: "/secret.env%5C", canonical: true, wantStatus: http.StatusNotFound},
486
+ {path: `/secret.env\`, canonical: true, wantStatus: http.StatusNotFound},
487
+ {path: "/secret.env/", acceptEncoding: "gzip", wantStatus: http.StatusNotFound},
488
+ {path: "/secret.env/", passThru: true, wantNext: true},
489
+ {path: "/secret.env/", canonical: true, wantStatus: http.StatusPermanentRedirect, wantLocation: "/secret.env"},
490
+ {path: "/dir/", wantStatus: http.StatusOK},
491
+ {path: "/dir", wantStatus: http.StatusOK},
492
+ {path: "/dir/index.html/", wantStatus: http.StatusNotFound},
493
+ } {
494
+ fsrv := FileServer{
495
+ Root: root,
496
+ CanonicalURIs: &tc.canonical,
497
+ PassThru: tc.passThru,
498
+ PrecompressedOrder: []string{"gzip"},
499
+ }
500
+ ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
501
+ if err := fsrv.Provision(ctx); err != nil {
502
+ t.Fatal(err)
503
+ }
504
+ fsrv.precompressors = map[string]encode.Precompressed{
505
+ "gzip": testPrecompressed{encoding: "gzip", suffix: ".gz"},
506
+ }
507
+
508
+ r := httptest.NewRequest(http.MethodGet, tc.path, nil)
509
+ r.Header.Set("Accept-Encoding", tc.acceptEncoding)
510
+ reqCtx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, caddy.NewReplacer())
511
+ reqCtx = context.WithValue(reqCtx, caddyhttp.OriginalRequestCtxKey, *r)
512
+ r = r.WithContext(reqCtx)
513
+
514
+ var calledNext bool
515
+ next := caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
516
+ calledNext = true
517
+ return nil
518
+ })
519
+
520
+ w := httptest.NewRecorder()
521
+ err := fsrv.ServeHTTP(w, r, next)
522
+ cancel()
523
+
524
+ if tc.wantNext {
525
+ if err != nil || !calledNext {
526
+ t.Errorf("Test %d (%s): expected the next handler, got called=%t err=%v", i, tc.path, calledNext, err)
527
+ }
528
+ continue
529
+ }
530
+ status := w.Code
531
+ if he, ok := errors.AsType[caddyhttp.HandlerError](err); ok {
532
+ status = he.StatusCode
533
+ } else if err != nil {
534
+ t.Errorf("Test %d (%s): unexpected error: %v", i, tc.path, err)
535
+ continue
536
+ }
537
+ if status != tc.wantStatus {
538
+ t.Errorf("Test %d (%s): expected status %d, got %d", i, tc.path, tc.wantStatus, status)
539
+ }
540
+ if got := w.Header().Get("Location"); got != tc.wantLocation {
541
+ t.Errorf("Test %d (%s): expected Location %q, got %q", i, tc.path, tc.wantLocation, got)
542
+ }
543
+ }
544
+}
545
+
546
+// The trailing separator of a filesystem root such as "/" or `C:\` must be
547
+// kept, else `C:` resolves to the current directory of the drive.
548
+func TestServeHTTPFilesystemRoot(t *testing.T) {
549
+ wd, err := os.Getwd()
550
+ if err != nil {
551
+ t.Fatal(err)
552
+ }
553
+ volume := filepath.VolumeName(wd)
554
+ // top-level directory leading to wd, listed only if the root is served
555
+ topDir := strings.Split(strings.TrimPrefix(wd[len(volume):], separator), separator)[0]
556
+
557
+ roots := []string{volume + separator}
558
+ if runtime.GOOS == "windows" {
559
+ roots = append(roots, separator)
560
+ }
561
+ for _, root := range roots {
562
+ t.Run(root, func(t *testing.T) {
563
+ fsrv := FileServer{Root: root, Browse: &Browse{}}
564
+ ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
565
+ defer cancel()
566
+ if err := fsrv.Provision(ctx); err != nil {
567
+ t.Fatal(err)
568
+ }
569
+
570
+ r := httptest.NewRequest(http.MethodGet, "/", nil)
571
+ reqCtx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, caddy.NewReplacer())
572
+ reqCtx = context.WithValue(reqCtx, caddyhttp.OriginalRequestCtxKey, *r)
573
+ r = r.WithContext(reqCtx)
574
+ r.Header.Set("Accept", "application/json")
575
+ w := httptest.NewRecorder()
576
+ if err := fsrv.ServeHTTP(w, r, nil); err != nil {
577
+ t.Fatal(err)
578
+ }
579
+ if w.Code != http.StatusOK {
580
+ t.Fatalf("status = %d, want %d", w.Code, http.StatusOK)
581
+ }
582
+
583
+ var items []struct {
584
+ Name string `json:"name"`
585
+ }
586
+ if err := json.Unmarshal(w.Body.Bytes(), &items); err != nil {
587
+ t.Fatal(err)
588
+ }
589
+ for _, item := range items {
590
+ if strings.TrimSuffix(item.Name, "/") == topDir {
591
+ return
592
+ }
593
+ }
594
+ t.Errorf("listing of %q doesn't contain %q: %s", root, topDir, w.Body.String())
595
+ })
596
+ }
597
+}