github.com/ck00004/CobaltStrikeParser-Go@v1.0.14/lib/http/fs_test.go (about) 1 // Copyright 2010 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package http_test 6 7 import ( 8 "bufio" 9 "bytes" 10 "errors" 11 "fmt" 12 "io" 13 "io/fs" 14 "mime" 15 "mime/multipart" 16 "net" 17 "os" 18 "os/exec" 19 "path" 20 "path/filepath" 21 "reflect" 22 "regexp" 23 "runtime" 24 "strings" 25 "testing" 26 "time" 27 28 "github.com/ck00004/CobaltStrikeParser-Go/lib/url" 29 30 . "github.com/ck00004/CobaltStrikeParser-Go/lib/http" 31 32 "github.com/ck00004/CobaltStrikeParser-Go/lib/http/httptest" 33 ) 34 35 const ( 36 testFile = "testdata/file" 37 testFileLen = 11 38 ) 39 40 type wantRange struct { 41 start, end int64 // range [start,end) 42 } 43 44 var ServeFileRangeTests = []struct { 45 r string 46 code int 47 ranges []wantRange 48 }{ 49 {r: "", code: StatusOK}, 50 {r: "bytes=0-4", code: StatusPartialContent, ranges: []wantRange{{0, 5}}}, 51 {r: "bytes=2-", code: StatusPartialContent, ranges: []wantRange{{2, testFileLen}}}, 52 {r: "bytes=-5", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 5, testFileLen}}}, 53 {r: "bytes=3-7", code: StatusPartialContent, ranges: []wantRange{{3, 8}}}, 54 {r: "bytes=0-0,-2", code: StatusPartialContent, ranges: []wantRange{{0, 1}, {testFileLen - 2, testFileLen}}}, 55 {r: "bytes=0-1,5-8", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, 9}}}, 56 {r: "bytes=0-1,5-", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, testFileLen}}}, 57 {r: "bytes=5-1000", code: StatusPartialContent, ranges: []wantRange{{5, testFileLen}}}, 58 {r: "bytes=0-,1-,2-,3-,4-", code: StatusOK}, // ignore wasteful range request 59 {r: "bytes=0-9", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen - 1}}}, 60 {r: "bytes=0-10", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}}, 61 {r: "bytes=0-11", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}}, 62 {r: "bytes=10-11", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}}, 63 {r: "bytes=10-", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}}, 64 {r: "bytes=11-", code: StatusRequestedRangeNotSatisfiable}, 65 {r: "bytes=11-12", code: StatusRequestedRangeNotSatisfiable}, 66 {r: "bytes=12-12", code: StatusRequestedRangeNotSatisfiable}, 67 {r: "bytes=11-100", code: StatusRequestedRangeNotSatisfiable}, 68 {r: "bytes=12-100", code: StatusRequestedRangeNotSatisfiable}, 69 {r: "bytes=100-", code: StatusRequestedRangeNotSatisfiable}, 70 {r: "bytes=100-1000", code: StatusRequestedRangeNotSatisfiable}, 71 } 72 73 func TestServeFile(t *testing.T) { 74 setParallel(t) 75 defer afterTest(t) 76 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { 77 ServeFile(w, r, "testdata/file") 78 })) 79 defer ts.Close() 80 c := ts.Client() 81 82 var err error 83 84 file, err := os.ReadFile(testFile) 85 if err != nil { 86 t.Fatal("reading file:", err) 87 } 88 89 // set up the Request (re-used for all tests) 90 var req Request 91 req.Header = make(Header) 92 if req.URL, err = url.Parse(ts.URL); err != nil { 93 t.Fatal("ParseURL:", err) 94 } 95 req.Method = "GET" 96 97 // straight GET 98 _, body := getBody(t, "straight get", req, c) 99 if !bytes.Equal(body, file) { 100 t.Fatalf("body mismatch: got %q, want %q", body, file) 101 } 102 103 // Range tests 104 Cases: 105 for _, rt := range ServeFileRangeTests { 106 if rt.r != "" { 107 req.Header.Set("Range", rt.r) 108 } 109 resp, body := getBody(t, fmt.Sprintf("range test %q", rt.r), req, c) 110 if resp.StatusCode != rt.code { 111 t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, resp.StatusCode, rt.code) 112 } 113 if rt.code == StatusRequestedRangeNotSatisfiable { 114 continue 115 } 116 wantContentRange := "" 117 if len(rt.ranges) == 1 { 118 rng := rt.ranges[0] 119 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen) 120 } 121 cr := resp.Header.Get("Content-Range") 122 if cr != wantContentRange { 123 t.Errorf("range=%q: Content-Range = %q, want %q", rt.r, cr, wantContentRange) 124 } 125 ct := resp.Header.Get("Content-Type") 126 if len(rt.ranges) == 1 { 127 rng := rt.ranges[0] 128 wantBody := file[rng.start:rng.end] 129 if !bytes.Equal(body, wantBody) { 130 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody) 131 } 132 if strings.HasPrefix(ct, "multipart/byteranges") { 133 t.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt.r, ct) 134 } 135 } 136 if len(rt.ranges) > 1 { 137 typ, params, err := mime.ParseMediaType(ct) 138 if err != nil { 139 t.Errorf("range=%q content-type = %q; %v", rt.r, ct, err) 140 continue 141 } 142 if typ != "multipart/byteranges" { 143 t.Errorf("range=%q content-type = %q; want multipart/byteranges", rt.r, typ) 144 continue 145 } 146 if params["boundary"] == "" { 147 t.Errorf("range=%q content-type = %q; lacks boundary", rt.r, ct) 148 continue 149 } 150 if g, w := resp.ContentLength, int64(len(body)); g != w { 151 t.Errorf("range=%q Content-Length = %d; want %d", rt.r, g, w) 152 continue 153 } 154 mr := multipart.NewReader(bytes.NewReader(body), params["boundary"]) 155 for ri, rng := range rt.ranges { 156 part, err := mr.NextPart() 157 if err != nil { 158 t.Errorf("range=%q, reading part index %d: %v", rt.r, ri, err) 159 continue Cases 160 } 161 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen) 162 if g, w := part.Header.Get("Content-Range"), wantContentRange; g != w { 163 t.Errorf("range=%q: part Content-Range = %q; want %q", rt.r, g, w) 164 } 165 body, err := io.ReadAll(part) 166 if err != nil { 167 t.Errorf("range=%q, reading part index %d body: %v", rt.r, ri, err) 168 continue Cases 169 } 170 wantBody := file[rng.start:rng.end] 171 if !bytes.Equal(body, wantBody) { 172 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody) 173 } 174 } 175 _, err = mr.NextPart() 176 if err != io.EOF { 177 t.Errorf("range=%q; expected final error io.EOF; got %v", rt.r, err) 178 } 179 } 180 } 181 } 182 183 func TestServeFile_DotDot(t *testing.T) { 184 tests := []struct { 185 req string 186 wantStatus int 187 }{ 188 {"/testdata/file", 200}, 189 {"/../file", 400}, 190 {"/..", 400}, 191 {"/../", 400}, 192 {"/../foo", 400}, 193 {"/..\\foo", 400}, 194 {"/file/a", 200}, 195 {"/file/a..", 200}, 196 {"/file/a/..", 400}, 197 {"/file/a\\..", 400}, 198 } 199 for _, tt := range tests { 200 req, err := ReadRequest(bufio.NewReader(strings.NewReader("GET " + tt.req + " HTTP/1.1\r\nHost: foo\r\n\r\n"))) 201 if err != nil { 202 t.Errorf("bad request %q: %v", tt.req, err) 203 continue 204 } 205 rec := httptest.NewRecorder() 206 ServeFile(rec, req, "testdata/file") 207 if rec.Code != tt.wantStatus { 208 t.Errorf("for request %q, status = %d; want %d", tt.req, rec.Code, tt.wantStatus) 209 } 210 } 211 } 212 213 // Tests that this doesn't panic. (Issue 30165) 214 func TestServeFileDirPanicEmptyPath(t *testing.T) { 215 rec := httptest.NewRecorder() 216 req := httptest.NewRequest("GET", "/", nil) 217 req.URL.Path = "" 218 ServeFile(rec, req, "testdata") 219 res := rec.Result() 220 if res.StatusCode != 301 { 221 t.Errorf("code = %v; want 301", res.Status) 222 } 223 } 224 225 var fsRedirectTestData = []struct { 226 original, redirect string 227 }{ 228 {"/test/index.html", "/test/"}, 229 {"/test/testdata", "/test/testdata/"}, 230 {"/test/testdata/file/", "/test/testdata/file"}, 231 } 232 233 func TestFSRedirect(t *testing.T) { 234 defer afterTest(t) 235 ts := httptest.NewServer(StripPrefix("/test", FileServer(Dir(".")))) 236 defer ts.Close() 237 238 for _, data := range fsRedirectTestData { 239 res, err := Get(ts.URL + data.original) 240 if err != nil { 241 t.Fatal(err) 242 } 243 res.Body.Close() 244 if g, e := res.Request.URL.Path, data.redirect; g != e { 245 t.Errorf("redirect from %s: got %s, want %s", data.original, g, e) 246 } 247 } 248 } 249 250 type testFileSystem struct { 251 open func(name string) (File, error) 252 } 253 254 func (fs *testFileSystem) Open(name string) (File, error) { 255 return fs.open(name) 256 } 257 258 func TestFileServerCleans(t *testing.T) { 259 defer afterTest(t) 260 ch := make(chan string, 1) 261 fs := FileServer(&testFileSystem{func(name string) (File, error) { 262 ch <- name 263 return nil, errors.New("file does not exist") 264 }}) 265 tests := []struct { 266 reqPath, openArg string 267 }{ 268 {"/foo.txt", "/foo.txt"}, 269 {"//foo.txt", "/foo.txt"}, 270 {"/../foo.txt", "/foo.txt"}, 271 } 272 req, _ := NewRequest("GET", "http://example.com", nil) 273 for n, test := range tests { 274 rec := httptest.NewRecorder() 275 req.URL.Path = test.reqPath 276 fs.ServeHTTP(rec, req) 277 if got := <-ch; got != test.openArg { 278 t.Errorf("test %d: got %q, want %q", n, got, test.openArg) 279 } 280 } 281 } 282 283 func TestFileServerEscapesNames(t *testing.T) { 284 defer afterTest(t) 285 const dirListPrefix = "<pre>\n" 286 const dirListSuffix = "\n</pre>\n" 287 tests := []struct { 288 name, escaped string 289 }{ 290 {`simple_name`, `<a href="simple_name">simple_name</a>`}, 291 {`"'<>&`, `<a href="%22%27%3C%3E&">"'<>&</a>`}, 292 {`?foo=bar#baz`, `<a href="%3Ffoo=bar%23baz">?foo=bar#baz</a>`}, 293 {`<combo>?foo`, `<a href="%3Ccombo%3E%3Ffoo"><combo>?foo</a>`}, 294 {`foo:bar`, `<a href="./foo:bar">foo:bar</a>`}, 295 } 296 297 // We put each test file in its own directory in the fakeFS so we can look at it in isolation. 298 fs := make(fakeFS) 299 for i, test := range tests { 300 testFile := &fakeFileInfo{basename: test.name} 301 fs[fmt.Sprintf("/%d", i)] = &fakeFileInfo{ 302 dir: true, 303 modtime: time.Unix(1000000000, 0).UTC(), 304 ents: []*fakeFileInfo{testFile}, 305 } 306 fs[fmt.Sprintf("/%d/%s", i, test.name)] = testFile 307 } 308 309 ts := httptest.NewServer(FileServer(&fs)) 310 defer ts.Close() 311 for i, test := range tests { 312 url := fmt.Sprintf("%s/%d", ts.URL, i) 313 res, err := Get(url) 314 if err != nil { 315 t.Fatalf("test %q: Get: %v", test.name, err) 316 } 317 b, err := io.ReadAll(res.Body) 318 if err != nil { 319 t.Fatalf("test %q: read Body: %v", test.name, err) 320 } 321 s := string(b) 322 if !strings.HasPrefix(s, dirListPrefix) || !strings.HasSuffix(s, dirListSuffix) { 323 t.Errorf("test %q: listing dir, full output is %q, want prefix %q and suffix %q", test.name, s, dirListPrefix, dirListSuffix) 324 } 325 if trimmed := strings.TrimSuffix(strings.TrimPrefix(s, dirListPrefix), dirListSuffix); trimmed != test.escaped { 326 t.Errorf("test %q: listing dir, filename escaped to %q, want %q", test.name, trimmed, test.escaped) 327 } 328 res.Body.Close() 329 } 330 } 331 332 func TestFileServerSortsNames(t *testing.T) { 333 defer afterTest(t) 334 const contents = "I am a fake file" 335 dirMod := time.Unix(123, 0).UTC() 336 fileMod := time.Unix(1000000000, 0).UTC() 337 fs := fakeFS{ 338 "/": &fakeFileInfo{ 339 dir: true, 340 modtime: dirMod, 341 ents: []*fakeFileInfo{ 342 { 343 basename: "b", 344 modtime: fileMod, 345 contents: contents, 346 }, 347 { 348 basename: "a", 349 modtime: fileMod, 350 contents: contents, 351 }, 352 }, 353 }, 354 } 355 356 ts := httptest.NewServer(FileServer(&fs)) 357 defer ts.Close() 358 359 res, err := Get(ts.URL) 360 if err != nil { 361 t.Fatalf("Get: %v", err) 362 } 363 defer res.Body.Close() 364 365 b, err := io.ReadAll(res.Body) 366 if err != nil { 367 t.Fatalf("read Body: %v", err) 368 } 369 s := string(b) 370 if !strings.Contains(s, "<a href=\"a\">a</a>\n<a href=\"b\">b</a>") { 371 t.Errorf("output appears to be unsorted:\n%s", s) 372 } 373 } 374 375 func mustRemoveAll(dir string) { 376 err := os.RemoveAll(dir) 377 if err != nil { 378 panic(err) 379 } 380 } 381 382 func TestFileServerImplicitLeadingSlash(t *testing.T) { 383 defer afterTest(t) 384 tempDir := t.TempDir() 385 if err := os.WriteFile(filepath.Join(tempDir, "foo.txt"), []byte("Hello world"), 0644); err != nil { 386 t.Fatalf("WriteFile: %v", err) 387 } 388 ts := httptest.NewServer(StripPrefix("/bar/", FileServer(Dir(tempDir)))) 389 defer ts.Close() 390 get := func(suffix string) string { 391 res, err := Get(ts.URL + suffix) 392 if err != nil { 393 t.Fatalf("Get %s: %v", suffix, err) 394 } 395 b, err := io.ReadAll(res.Body) 396 if err != nil { 397 t.Fatalf("ReadAll %s: %v", suffix, err) 398 } 399 res.Body.Close() 400 return string(b) 401 } 402 if s := get("/bar/"); !strings.Contains(s, ">foo.txt<") { 403 t.Logf("expected a directory listing with foo.txt, got %q", s) 404 } 405 if s := get("/bar/foo.txt"); s != "Hello world" { 406 t.Logf("expected %q, got %q", "Hello world", s) 407 } 408 } 409 410 func TestDirJoin(t *testing.T) { 411 if runtime.GOOS == "windows" { 412 t.Skip("skipping test on windows") 413 } 414 wfi, err := os.Stat("/etc/hosts") 415 if err != nil { 416 t.Skip("skipping test; no /etc/hosts file") 417 } 418 test := func(d Dir, name string) { 419 f, err := d.Open(name) 420 if err != nil { 421 t.Fatalf("open of %s: %v", name, err) 422 } 423 defer f.Close() 424 gfi, err := f.Stat() 425 if err != nil { 426 t.Fatalf("stat of %s: %v", name, err) 427 } 428 if !os.SameFile(gfi, wfi) { 429 t.Errorf("%s got different file", name) 430 } 431 } 432 test(Dir("/etc/"), "/hosts") 433 test(Dir("/etc/"), "hosts") 434 test(Dir("/etc/"), "../../../../hosts") 435 test(Dir("/etc"), "/hosts") 436 test(Dir("/etc"), "hosts") 437 test(Dir("/etc"), "../../../../hosts") 438 439 // Not really directories, but since we use this trick in 440 // ServeFile, test it: 441 test(Dir("/etc/hosts"), "") 442 test(Dir("/etc/hosts"), "/") 443 test(Dir("/etc/hosts"), "../") 444 } 445 446 func TestEmptyDirOpenCWD(t *testing.T) { 447 test := func(d Dir) { 448 name := "fs_test.go" 449 f, err := d.Open(name) 450 if err != nil { 451 t.Fatalf("open of %s: %v", name, err) 452 } 453 defer f.Close() 454 } 455 test(Dir("")) 456 test(Dir(".")) 457 test(Dir("./")) 458 } 459 460 func TestServeFileContentType(t *testing.T) { 461 defer afterTest(t) 462 const ctype = "icecream/chocolate" 463 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { 464 switch r.FormValue("override") { 465 case "1": 466 w.Header().Set("Content-Type", ctype) 467 case "2": 468 // Explicitly inhibit sniffing. 469 w.Header()["Content-Type"] = []string{} 470 } 471 ServeFile(w, r, "testdata/file") 472 })) 473 defer ts.Close() 474 get := func(override string, want []string) { 475 resp, err := Get(ts.URL + "?override=" + override) 476 if err != nil { 477 t.Fatal(err) 478 } 479 if h := resp.Header["Content-Type"]; !reflect.DeepEqual(h, want) { 480 t.Errorf("Content-Type mismatch: got %v, want %v", h, want) 481 } 482 resp.Body.Close() 483 } 484 get("0", []string{"text/plain; charset=utf-8"}) 485 get("1", []string{ctype}) 486 get("2", nil) 487 } 488 489 func TestServeFileMimeType(t *testing.T) { 490 defer afterTest(t) 491 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { 492 ServeFile(w, r, "testdata/style.css") 493 })) 494 defer ts.Close() 495 resp, err := Get(ts.URL) 496 if err != nil { 497 t.Fatal(err) 498 } 499 resp.Body.Close() 500 want := "text/css; charset=utf-8" 501 if h := resp.Header.Get("Content-Type"); h != want { 502 t.Errorf("Content-Type mismatch: got %q, want %q", h, want) 503 } 504 } 505 506 func TestServeFileFromCWD(t *testing.T) { 507 defer afterTest(t) 508 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { 509 ServeFile(w, r, "fs_test.go") 510 })) 511 defer ts.Close() 512 r, err := Get(ts.URL) 513 if err != nil { 514 t.Fatal(err) 515 } 516 r.Body.Close() 517 if r.StatusCode != 200 { 518 t.Fatalf("expected 200 OK, got %s", r.Status) 519 } 520 } 521 522 // Issue 13996 523 func TestServeDirWithoutTrailingSlash(t *testing.T) { 524 e := "/testdata/" 525 defer afterTest(t) 526 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { 527 ServeFile(w, r, ".") 528 })) 529 defer ts.Close() 530 r, err := Get(ts.URL + "/testdata") 531 if err != nil { 532 t.Fatal(err) 533 } 534 r.Body.Close() 535 if g := r.Request.URL.Path; g != e { 536 t.Errorf("got %s, want %s", g, e) 537 } 538 } 539 540 // Tests that ServeFile doesn't add a Content-Length if a Content-Encoding is 541 // specified. 542 func TestServeFileWithContentEncoding_h1(t *testing.T) { testServeFileWithContentEncoding(t, h1Mode) } 543 func TestServeFileWithContentEncoding_h2(t *testing.T) { testServeFileWithContentEncoding(t, h2Mode) } 544 func testServeFileWithContentEncoding(t *testing.T, h2 bool) { 545 defer afterTest(t) 546 cst := newClientServerTest(t, h2, HandlerFunc(func(w ResponseWriter, r *Request) { 547 w.Header().Set("Content-Encoding", "foo") 548 ServeFile(w, r, "testdata/file") 549 550 // Because the testdata is so small, it would fit in 551 // both the h1 and h2 Server's write buffers. For h1, 552 // sendfile is used, though, forcing a header flush at 553 // the io.Copy. http2 doesn't do a header flush so 554 // buffers all 11 bytes and then adds its own 555 // Content-Length. To prevent the Server's 556 // Content-Length and test ServeFile only, flush here. 557 w.(Flusher).Flush() 558 })) 559 defer cst.close() 560 resp, err := cst.c.Get(cst.ts.URL) 561 if err != nil { 562 t.Fatal(err) 563 } 564 resp.Body.Close() 565 if g, e := resp.ContentLength, int64(-1); g != e { 566 t.Errorf("Content-Length mismatch: got %d, want %d", g, e) 567 } 568 } 569 570 func TestServeIndexHtml(t *testing.T) { 571 defer afterTest(t) 572 573 for i := 0; i < 2; i++ { 574 var h Handler 575 var name string 576 switch i { 577 case 0: 578 h = FileServer(Dir(".")) 579 name = "Dir" 580 case 1: 581 h = FileServer(FS(os.DirFS("."))) 582 name = "DirFS" 583 } 584 t.Run(name, func(t *testing.T) { 585 const want = "index.html says hello\n" 586 ts := httptest.NewServer(h) 587 defer ts.Close() 588 589 for _, path := range []string{"/testdata/", "/testdata/index.html"} { 590 res, err := Get(ts.URL + path) 591 if err != nil { 592 t.Fatal(err) 593 } 594 b, err := io.ReadAll(res.Body) 595 if err != nil { 596 t.Fatal("reading Body:", err) 597 } 598 if s := string(b); s != want { 599 t.Errorf("for path %q got %q, want %q", path, s, want) 600 } 601 res.Body.Close() 602 } 603 }) 604 } 605 } 606 607 func TestServeIndexHtmlFS(t *testing.T) { 608 defer afterTest(t) 609 const want = "index.html says hello\n" 610 ts := httptest.NewServer(FileServer(Dir("."))) 611 defer ts.Close() 612 613 for _, path := range []string{"/testdata/", "/testdata/index.html"} { 614 res, err := Get(ts.URL + path) 615 if err != nil { 616 t.Fatal(err) 617 } 618 b, err := io.ReadAll(res.Body) 619 if err != nil { 620 t.Fatal("reading Body:", err) 621 } 622 if s := string(b); s != want { 623 t.Errorf("for path %q got %q, want %q", path, s, want) 624 } 625 res.Body.Close() 626 } 627 } 628 629 func TestFileServerZeroByte(t *testing.T) { 630 defer afterTest(t) 631 ts := httptest.NewServer(FileServer(Dir("."))) 632 defer ts.Close() 633 634 c, err := net.Dial("tcp", ts.Listener.Addr().String()) 635 if err != nil { 636 t.Fatal(err) 637 } 638 defer c.Close() 639 _, err = fmt.Fprintf(c, "GET /..\x00 HTTP/1.0\r\n\r\n") 640 if err != nil { 641 t.Fatal(err) 642 } 643 var got bytes.Buffer 644 bufr := bufio.NewReader(io.TeeReader(c, &got)) 645 res, err := ReadResponse(bufr, nil) 646 if err != nil { 647 t.Fatal("ReadResponse: ", err) 648 } 649 if res.StatusCode == 200 { 650 t.Errorf("got status 200; want an error. Body is:\n%s", got.Bytes()) 651 } 652 } 653 654 type fakeFileInfo struct { 655 dir bool 656 basename string 657 modtime time.Time 658 ents []*fakeFileInfo 659 contents string 660 err error 661 } 662 663 func (f *fakeFileInfo) Name() string { return f.basename } 664 func (f *fakeFileInfo) Sys() interface{} { return nil } 665 func (f *fakeFileInfo) ModTime() time.Time { return f.modtime } 666 func (f *fakeFileInfo) IsDir() bool { return f.dir } 667 func (f *fakeFileInfo) Size() int64 { return int64(len(f.contents)) } 668 func (f *fakeFileInfo) Mode() fs.FileMode { 669 if f.dir { 670 return 0755 | fs.ModeDir 671 } 672 return 0644 673 } 674 675 type fakeFile struct { 676 io.ReadSeeker 677 fi *fakeFileInfo 678 path string // as opened 679 entpos int 680 } 681 682 func (f *fakeFile) Close() error { return nil } 683 func (f *fakeFile) Stat() (fs.FileInfo, error) { return f.fi, nil } 684 func (f *fakeFile) Readdir(count int) ([]fs.FileInfo, error) { 685 if !f.fi.dir { 686 return nil, fs.ErrInvalid 687 } 688 var fis []fs.FileInfo 689 690 limit := f.entpos + count 691 if count <= 0 || limit > len(f.fi.ents) { 692 limit = len(f.fi.ents) 693 } 694 for ; f.entpos < limit; f.entpos++ { 695 fis = append(fis, f.fi.ents[f.entpos]) 696 } 697 698 if len(fis) == 0 && count > 0 { 699 return fis, io.EOF 700 } else { 701 return fis, nil 702 } 703 } 704 705 type fakeFS map[string]*fakeFileInfo 706 707 func (fsys fakeFS) Open(name string) (File, error) { 708 name = path.Clean(name) 709 f, ok := fsys[name] 710 if !ok { 711 return nil, fs.ErrNotExist 712 } 713 if f.err != nil { 714 return nil, f.err 715 } 716 return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil 717 } 718 719 func TestDirectoryIfNotModified(t *testing.T) { 720 defer afterTest(t) 721 const indexContents = "I am a fake index.html file" 722 fileMod := time.Unix(1000000000, 0).UTC() 723 fileModStr := fileMod.Format(TimeFormat) 724 dirMod := time.Unix(123, 0).UTC() 725 indexFile := &fakeFileInfo{ 726 basename: "index.html", 727 modtime: fileMod, 728 contents: indexContents, 729 } 730 fs := fakeFS{ 731 "/": &fakeFileInfo{ 732 dir: true, 733 modtime: dirMod, 734 ents: []*fakeFileInfo{indexFile}, 735 }, 736 "/index.html": indexFile, 737 } 738 739 ts := httptest.NewServer(FileServer(fs)) 740 defer ts.Close() 741 742 res, err := Get(ts.URL) 743 if err != nil { 744 t.Fatal(err) 745 } 746 b, err := io.ReadAll(res.Body) 747 if err != nil { 748 t.Fatal(err) 749 } 750 if string(b) != indexContents { 751 t.Fatalf("Got body %q; want %q", b, indexContents) 752 } 753 res.Body.Close() 754 755 lastMod := res.Header.Get("Last-Modified") 756 if lastMod != fileModStr { 757 t.Fatalf("initial Last-Modified = %q; want %q", lastMod, fileModStr) 758 } 759 760 req, _ := NewRequest("GET", ts.URL, nil) 761 req.Header.Set("If-Modified-Since", lastMod) 762 763 c := ts.Client() 764 res, err = c.Do(req) 765 if err != nil { 766 t.Fatal(err) 767 } 768 if res.StatusCode != 304 { 769 t.Fatalf("Code after If-Modified-Since request = %v; want 304", res.StatusCode) 770 } 771 res.Body.Close() 772 773 // Advance the index.html file's modtime, but not the directory's. 774 indexFile.modtime = indexFile.modtime.Add(1 * time.Hour) 775 776 res, err = c.Do(req) 777 if err != nil { 778 t.Fatal(err) 779 } 780 if res.StatusCode != 200 { 781 t.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res.StatusCode, res) 782 } 783 res.Body.Close() 784 } 785 786 func mustStat(t *testing.T, fileName string) fs.FileInfo { 787 fi, err := os.Stat(fileName) 788 if err != nil { 789 t.Fatal(err) 790 } 791 return fi 792 } 793 794 func TestServeContent(t *testing.T) { 795 defer afterTest(t) 796 type serveParam struct { 797 name string 798 modtime time.Time 799 content io.ReadSeeker 800 contentType string 801 etag string 802 } 803 servec := make(chan serveParam, 1) 804 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { 805 p := <-servec 806 if p.etag != "" { 807 w.Header().Set("ETag", p.etag) 808 } 809 if p.contentType != "" { 810 w.Header().Set("Content-Type", p.contentType) 811 } 812 ServeContent(w, r, p.name, p.modtime, p.content) 813 })) 814 defer ts.Close() 815 816 type testCase struct { 817 // One of file or content must be set: 818 file string 819 content io.ReadSeeker 820 821 modtime time.Time 822 serveETag string // optional 823 serveContentType string // optional 824 reqHeader map[string]string 825 wantLastMod string 826 wantContentType string 827 wantContentRange string 828 wantStatus int 829 } 830 htmlModTime := mustStat(t, "testdata/index.html").ModTime() 831 tests := map[string]testCase{ 832 "no_last_modified": { 833 file: "testdata/style.css", 834 wantContentType: "text/css; charset=utf-8", 835 wantStatus: 200, 836 }, 837 "with_last_modified": { 838 file: "testdata/index.html", 839 wantContentType: "text/html; charset=utf-8", 840 modtime: htmlModTime, 841 wantLastMod: htmlModTime.UTC().Format(TimeFormat), 842 wantStatus: 200, 843 }, 844 "not_modified_modtime": { 845 file: "testdata/style.css", 846 serveETag: `"foo"`, // Last-Modified sent only when no ETag 847 modtime: htmlModTime, 848 reqHeader: map[string]string{ 849 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat), 850 }, 851 wantStatus: 304, 852 }, 853 "not_modified_modtime_with_contenttype": { 854 file: "testdata/style.css", 855 serveContentType: "text/css", // explicit content type 856 serveETag: `"foo"`, // Last-Modified sent only when no ETag 857 modtime: htmlModTime, 858 reqHeader: map[string]string{ 859 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat), 860 }, 861 wantStatus: 304, 862 }, 863 "not_modified_etag": { 864 file: "testdata/style.css", 865 serveETag: `"foo"`, 866 reqHeader: map[string]string{ 867 "If-None-Match": `"foo"`, 868 }, 869 wantStatus: 304, 870 }, 871 "not_modified_etag_no_seek": { 872 content: panicOnSeek{nil}, // should never be called 873 serveETag: `W/"foo"`, // If-None-Match uses weak ETag comparison 874 reqHeader: map[string]string{ 875 "If-None-Match": `"baz", W/"foo"`, 876 }, 877 wantStatus: 304, 878 }, 879 "if_none_match_mismatch": { 880 file: "testdata/style.css", 881 serveETag: `"foo"`, 882 reqHeader: map[string]string{ 883 "If-None-Match": `"Foo"`, 884 }, 885 wantStatus: 200, 886 wantContentType: "text/css; charset=utf-8", 887 }, 888 "if_none_match_malformed": { 889 file: "testdata/style.css", 890 serveETag: `"foo"`, 891 reqHeader: map[string]string{ 892 "If-None-Match": `,`, 893 }, 894 wantStatus: 200, 895 wantContentType: "text/css; charset=utf-8", 896 }, 897 "range_good": { 898 file: "testdata/style.css", 899 serveETag: `"A"`, 900 reqHeader: map[string]string{ 901 "Range": "bytes=0-4", 902 }, 903 wantStatus: StatusPartialContent, 904 wantContentType: "text/css; charset=utf-8", 905 wantContentRange: "bytes 0-4/8", 906 }, 907 "range_match": { 908 file: "testdata/style.css", 909 serveETag: `"A"`, 910 reqHeader: map[string]string{ 911 "Range": "bytes=0-4", 912 "If-Range": `"A"`, 913 }, 914 wantStatus: StatusPartialContent, 915 wantContentType: "text/css; charset=utf-8", 916 wantContentRange: "bytes 0-4/8", 917 }, 918 "range_match_weak_etag": { 919 file: "testdata/style.css", 920 serveETag: `W/"A"`, 921 reqHeader: map[string]string{ 922 "Range": "bytes=0-4", 923 "If-Range": `W/"A"`, 924 }, 925 wantStatus: 200, 926 wantContentType: "text/css; charset=utf-8", 927 }, 928 "range_no_overlap": { 929 file: "testdata/style.css", 930 serveETag: `"A"`, 931 reqHeader: map[string]string{ 932 "Range": "bytes=10-20", 933 }, 934 wantStatus: StatusRequestedRangeNotSatisfiable, 935 wantContentType: "text/plain; charset=utf-8", 936 wantContentRange: "bytes */8", 937 }, 938 // An If-Range resource for entity "A", but entity "B" is now current. 939 // The Range request should be ignored. 940 "range_no_match": { 941 file: "testdata/style.css", 942 serveETag: `"A"`, 943 reqHeader: map[string]string{ 944 "Range": "bytes=0-4", 945 "If-Range": `"B"`, 946 }, 947 wantStatus: 200, 948 wantContentType: "text/css; charset=utf-8", 949 }, 950 "range_with_modtime": { 951 file: "testdata/style.css", 952 modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC), 953 reqHeader: map[string]string{ 954 "Range": "bytes=0-4", 955 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT", 956 }, 957 wantStatus: StatusPartialContent, 958 wantContentType: "text/css; charset=utf-8", 959 wantContentRange: "bytes 0-4/8", 960 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT", 961 }, 962 "range_with_modtime_mismatch": { 963 file: "testdata/style.css", 964 modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC), 965 reqHeader: map[string]string{ 966 "Range": "bytes=0-4", 967 "If-Range": "Wed, 25 Jun 2014 17:12:19 GMT", 968 }, 969 wantStatus: StatusOK, 970 wantContentType: "text/css; charset=utf-8", 971 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT", 972 }, 973 "range_with_modtime_nanos": { 974 file: "testdata/style.css", 975 modtime: time.Date(2014, 6, 25, 17, 12, 18, 123 /* nanos */, time.UTC), 976 reqHeader: map[string]string{ 977 "Range": "bytes=0-4", 978 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT", 979 }, 980 wantStatus: StatusPartialContent, 981 wantContentType: "text/css; charset=utf-8", 982 wantContentRange: "bytes 0-4/8", 983 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT", 984 }, 985 "unix_zero_modtime": { 986 content: strings.NewReader("<html>foo"), 987 modtime: time.Unix(0, 0), 988 wantStatus: StatusOK, 989 wantContentType: "text/html; charset=utf-8", 990 }, 991 "ifmatch_matches": { 992 file: "testdata/style.css", 993 serveETag: `"A"`, 994 reqHeader: map[string]string{ 995 "If-Match": `"Z", "A"`, 996 }, 997 wantStatus: 200, 998 wantContentType: "text/css; charset=utf-8", 999 }, 1000 "ifmatch_star": { 1001 file: "testdata/style.css", 1002 serveETag: `"A"`, 1003 reqHeader: map[string]string{ 1004 "If-Match": `*`, 1005 }, 1006 wantStatus: 200, 1007 wantContentType: "text/css; charset=utf-8", 1008 }, 1009 "ifmatch_failed": { 1010 file: "testdata/style.css", 1011 serveETag: `"A"`, 1012 reqHeader: map[string]string{ 1013 "If-Match": `"B"`, 1014 }, 1015 wantStatus: 412, 1016 }, 1017 "ifmatch_fails_on_weak_etag": { 1018 file: "testdata/style.css", 1019 serveETag: `W/"A"`, 1020 reqHeader: map[string]string{ 1021 "If-Match": `W/"A"`, 1022 }, 1023 wantStatus: 412, 1024 }, 1025 "if_unmodified_since_true": { 1026 file: "testdata/style.css", 1027 modtime: htmlModTime, 1028 reqHeader: map[string]string{ 1029 "If-Unmodified-Since": htmlModTime.UTC().Format(TimeFormat), 1030 }, 1031 wantStatus: 200, 1032 wantContentType: "text/css; charset=utf-8", 1033 wantLastMod: htmlModTime.UTC().Format(TimeFormat), 1034 }, 1035 "if_unmodified_since_false": { 1036 file: "testdata/style.css", 1037 modtime: htmlModTime, 1038 reqHeader: map[string]string{ 1039 "If-Unmodified-Since": htmlModTime.Add(-2 * time.Second).UTC().Format(TimeFormat), 1040 }, 1041 wantStatus: 412, 1042 wantLastMod: htmlModTime.UTC().Format(TimeFormat), 1043 }, 1044 } 1045 for testName, tt := range tests { 1046 var content io.ReadSeeker 1047 if tt.file != "" { 1048 f, err := os.Open(tt.file) 1049 if err != nil { 1050 t.Fatalf("test %q: %v", testName, err) 1051 } 1052 defer f.Close() 1053 content = f 1054 } else { 1055 content = tt.content 1056 } 1057 for _, method := range []string{"GET", "HEAD"} { 1058 //restore content in case it is consumed by previous method 1059 if content, ok := content.(*strings.Reader); ok { 1060 content.Seek(0, io.SeekStart) 1061 } 1062 1063 servec <- serveParam{ 1064 name: filepath.Base(tt.file), 1065 content: content, 1066 modtime: tt.modtime, 1067 etag: tt.serveETag, 1068 contentType: tt.serveContentType, 1069 } 1070 req, err := NewRequest(method, ts.URL, nil) 1071 if err != nil { 1072 t.Fatal(err) 1073 } 1074 for k, v := range tt.reqHeader { 1075 req.Header.Set(k, v) 1076 } 1077 1078 c := ts.Client() 1079 res, err := c.Do(req) 1080 if err != nil { 1081 t.Fatal(err) 1082 } 1083 io.Copy(io.Discard, res.Body) 1084 res.Body.Close() 1085 if res.StatusCode != tt.wantStatus { 1086 t.Errorf("test %q using %q: got status = %d; want %d", testName, method, res.StatusCode, tt.wantStatus) 1087 } 1088 if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e { 1089 t.Errorf("test %q using %q: got content-type = %q, want %q", testName, method, g, e) 1090 } 1091 if g, e := res.Header.Get("Content-Range"), tt.wantContentRange; g != e { 1092 t.Errorf("test %q using %q: got content-range = %q, want %q", testName, method, g, e) 1093 } 1094 if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e { 1095 t.Errorf("test %q using %q: got last-modified = %q, want %q", testName, method, g, e) 1096 } 1097 } 1098 } 1099 } 1100 1101 // Issue 12991 1102 func TestServerFileStatError(t *testing.T) { 1103 rec := httptest.NewRecorder() 1104 r, _ := NewRequest("GET", "http://foo/", nil) 1105 redirect := false 1106 name := "file.txt" 1107 fs := issue12991FS{} 1108 ExportServeFile(rec, r, fs, name, redirect) 1109 if body := rec.Body.String(); !strings.Contains(body, "403") || !strings.Contains(body, "Forbidden") { 1110 t.Errorf("wanted 403 forbidden message; got: %s", body) 1111 } 1112 } 1113 1114 type issue12991FS struct{} 1115 1116 func (issue12991FS) Open(string) (File, error) { return issue12991File{}, nil } 1117 1118 type issue12991File struct{ File } 1119 1120 func (issue12991File) Stat() (fs.FileInfo, error) { return nil, fs.ErrPermission } 1121 func (issue12991File) Close() error { return nil } 1122 1123 func TestServeContentErrorMessages(t *testing.T) { 1124 defer afterTest(t) 1125 fs := fakeFS{ 1126 "/500": &fakeFileInfo{ 1127 err: errors.New("random error"), 1128 }, 1129 "/403": &fakeFileInfo{ 1130 err: &fs.PathError{Err: fs.ErrPermission}, 1131 }, 1132 } 1133 ts := httptest.NewServer(FileServer(fs)) 1134 defer ts.Close() 1135 c := ts.Client() 1136 for _, code := range []int{403, 404, 500} { 1137 res, err := c.Get(fmt.Sprintf("%s/%d", ts.URL, code)) 1138 if err != nil { 1139 t.Errorf("Error fetching /%d: %v", code, err) 1140 continue 1141 } 1142 if res.StatusCode != code { 1143 t.Errorf("For /%d, status code = %d; want %d", code, res.StatusCode, code) 1144 } 1145 res.Body.Close() 1146 } 1147 } 1148 1149 // verifies that sendfile is being used on Linux 1150 func TestLinuxSendfile(t *testing.T) { 1151 setParallel(t) 1152 defer afterTest(t) 1153 if runtime.GOOS != "linux" { 1154 t.Skip("skipping; linux-only test") 1155 } 1156 if _, err := exec.LookPath("strace"); err != nil { 1157 t.Skip("skipping; strace not found in path") 1158 } 1159 1160 ln, err := net.Listen("tcp", "127.0.0.1:0") 1161 if err != nil { 1162 t.Fatal(err) 1163 } 1164 lnf, err := ln.(*net.TCPListener).File() 1165 if err != nil { 1166 t.Fatal(err) 1167 } 1168 defer ln.Close() 1169 1170 // Attempt to run strace, and skip on failure - this test requires SYS_PTRACE. 1171 if err := exec.Command("strace", "-f", "-q", os.Args[0], "-test.run=^$").Run(); err != nil { 1172 t.Skipf("skipping; failed to run strace: %v", err) 1173 } 1174 1175 filename := fmt.Sprintf("1kb-%d", os.Getpid()) 1176 filepath := path.Join(os.TempDir(), filename) 1177 1178 if err := os.WriteFile(filepath, bytes.Repeat([]byte{'a'}, 1<<10), 0755); err != nil { 1179 t.Fatal(err) 1180 } 1181 defer os.Remove(filepath) 1182 1183 var buf bytes.Buffer 1184 child := exec.Command("strace", "-f", "-q", os.Args[0], "-test.run=TestLinuxSendfileChild") 1185 child.ExtraFiles = append(child.ExtraFiles, lnf) 1186 child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...) 1187 child.Stdout = &buf 1188 child.Stderr = &buf 1189 if err := child.Start(); err != nil { 1190 t.Skipf("skipping; failed to start straced child: %v", err) 1191 } 1192 1193 res, err := Get(fmt.Sprintf("http://%s/%s", ln.Addr(), filename)) 1194 if err != nil { 1195 t.Fatalf("http client error: %v", err) 1196 } 1197 _, err = io.Copy(io.Discard, res.Body) 1198 if err != nil { 1199 t.Fatalf("client body read error: %v", err) 1200 } 1201 res.Body.Close() 1202 1203 // Force child to exit cleanly. 1204 Post(fmt.Sprintf("http://%s/quit", ln.Addr()), "", nil) 1205 child.Wait() 1206 1207 rx := regexp.MustCompile(`\b(n64:)?sendfile(64)?\(`) 1208 out := buf.String() 1209 if !rx.MatchString(out) { 1210 t.Errorf("no sendfile system call found in:\n%s", out) 1211 } 1212 } 1213 1214 func getBody(t *testing.T, testName string, req Request, client *Client) (*Response, []byte) { 1215 r, err := client.Do(&req) 1216 if err != nil { 1217 t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err) 1218 } 1219 b, err := io.ReadAll(r.Body) 1220 if err != nil { 1221 t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err) 1222 } 1223 return r, b 1224 } 1225 1226 // TestLinuxSendfileChild isn't a real test. It's used as a helper process 1227 // for TestLinuxSendfile. 1228 func TestLinuxSendfileChild(*testing.T) { 1229 if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { 1230 return 1231 } 1232 defer os.Exit(0) 1233 fd3 := os.NewFile(3, "ephemeral-port-listener") 1234 ln, err := net.FileListener(fd3) 1235 if err != nil { 1236 panic(err) 1237 } 1238 mux := NewServeMux() 1239 mux.Handle("/", FileServer(Dir(os.TempDir()))) 1240 mux.HandleFunc("/quit", func(ResponseWriter, *Request) { 1241 os.Exit(0) 1242 }) 1243 s := &Server{Handler: mux} 1244 err = s.Serve(ln) 1245 if err != nil { 1246 panic(err) 1247 } 1248 } 1249 1250 // Issue 18984: tests that requests for paths beyond files return not-found errors 1251 func TestFileServerNotDirError(t *testing.T) { 1252 defer afterTest(t) 1253 ts := httptest.NewServer(FileServer(Dir("testdata"))) 1254 defer ts.Close() 1255 1256 res, err := Get(ts.URL + "/index.html/not-a-file") 1257 if err != nil { 1258 t.Fatal(err) 1259 } 1260 res.Body.Close() 1261 if res.StatusCode != 404 { 1262 t.Errorf("StatusCode = %v; want 404", res.StatusCode) 1263 } 1264 1265 test := func(name string, dir Dir) { 1266 t.Run(name, func(t *testing.T) { 1267 _, err = dir.Open("/index.html/not-a-file") 1268 if err == nil { 1269 t.Fatal("err == nil; want != nil") 1270 } 1271 if !errors.Is(err, fs.ErrNotExist) { 1272 t.Errorf("err = %v; errors.Is(err, fs.ErrNotExist) = %v; want true", err, 1273 errors.Is(err, fs.ErrNotExist)) 1274 } 1275 1276 _, err = dir.Open("/index.html/not-a-dir/not-a-file") 1277 if err == nil { 1278 t.Fatal("err == nil; want != nil") 1279 } 1280 if !errors.Is(err, fs.ErrNotExist) { 1281 t.Errorf("err = %v; errors.Is(err, fs.ErrNotExist) = %v; want true", err, 1282 errors.Is(err, fs.ErrNotExist)) 1283 } 1284 }) 1285 } 1286 1287 absPath, err := filepath.Abs("testdata") 1288 if err != nil { 1289 t.Fatal("get abs path:", err) 1290 } 1291 1292 test("RelativePath", Dir("testdata")) 1293 test("AbsolutePath", Dir(absPath)) 1294 } 1295 1296 func TestFileServerCleanPath(t *testing.T) { 1297 tests := []struct { 1298 path string 1299 wantCode int 1300 wantOpen []string 1301 }{ 1302 {"/", 200, []string{"/", "/index.html"}}, 1303 {"/dir", 301, []string{"/dir"}}, 1304 {"/dir/", 200, []string{"/dir", "/dir/index.html"}}, 1305 } 1306 for _, tt := range tests { 1307 var log []string 1308 rr := httptest.NewRecorder() 1309 req, _ := NewRequest("GET", "http://foo.localhost"+tt.path, nil) 1310 FileServer(fileServerCleanPathDir{&log}).ServeHTTP(rr, req) 1311 if !reflect.DeepEqual(log, tt.wantOpen) { 1312 t.Logf("For %s: Opens = %q; want %q", tt.path, log, tt.wantOpen) 1313 } 1314 if rr.Code != tt.wantCode { 1315 t.Logf("For %s: Response code = %d; want %d", tt.path, rr.Code, tt.wantCode) 1316 } 1317 } 1318 } 1319 1320 type fileServerCleanPathDir struct { 1321 log *[]string 1322 } 1323 1324 func (d fileServerCleanPathDir) Open(path string) (File, error) { 1325 *(d.log) = append(*(d.log), path) 1326 if path == "/" || path == "/dir" || path == "/dir/" { 1327 // Just return back something that's a directory. 1328 return Dir(".").Open(".") 1329 } 1330 return nil, fs.ErrNotExist 1331 } 1332 1333 type panicOnSeek struct{ io.ReadSeeker } 1334 1335 func Test_scanETag(t *testing.T) { 1336 tests := []struct { 1337 in string 1338 wantETag string 1339 wantRemain string 1340 }{ 1341 {`W/"etag-1"`, `W/"etag-1"`, ""}, 1342 {`"etag-2"`, `"etag-2"`, ""}, 1343 {`"etag-1", "etag-2"`, `"etag-1"`, `, "etag-2"`}, 1344 {"", "", ""}, 1345 {"W/", "", ""}, 1346 {`W/"truc`, "", ""}, 1347 {`w/"case-sensitive"`, "", ""}, 1348 {`"spaced etag"`, "", ""}, 1349 } 1350 for _, test := range tests { 1351 etag, remain := ExportScanETag(test.in) 1352 if etag != test.wantETag || remain != test.wantRemain { 1353 t.Errorf("scanETag(%q)=%q %q, want %q %q", test.in, etag, remain, test.wantETag, test.wantRemain) 1354 } 1355 } 1356 } 1357 1358 // Issue 40940: Ensure that we only accept non-negative suffix-lengths 1359 // in "Range": "bytes=-N", and should reject "bytes=--2". 1360 func TestServeFileRejectsInvalidSuffixLengths_h1(t *testing.T) { 1361 testServeFileRejectsInvalidSuffixLengths(t, h1Mode) 1362 } 1363 func TestServeFileRejectsInvalidSuffixLengths_h2(t *testing.T) { 1364 testServeFileRejectsInvalidSuffixLengths(t, h2Mode) 1365 } 1366 1367 func testServeFileRejectsInvalidSuffixLengths(t *testing.T, h2 bool) { 1368 defer afterTest(t) 1369 cst := httptest.NewUnstartedServer(FileServer(Dir("testdata"))) 1370 cst.EnableHTTP2 = h2 1371 cst.StartTLS() 1372 defer cst.Close() 1373 1374 tests := []struct { 1375 r string 1376 wantCode int 1377 wantBody string 1378 }{ 1379 {"bytes=--6", 416, "invalid range\n"}, 1380 {"bytes=--0", 416, "invalid range\n"}, 1381 {"bytes=---0", 416, "invalid range\n"}, 1382 {"bytes=-6", 206, "hello\n"}, 1383 {"bytes=6-", 206, "html says hello\n"}, 1384 {"bytes=-6-", 416, "invalid range\n"}, 1385 {"bytes=-0", 206, ""}, 1386 {"bytes=", 200, "index.html says hello\n"}, 1387 } 1388 1389 for _, tt := range tests { 1390 tt := tt 1391 t.Run(tt.r, func(t *testing.T) { 1392 req, err := NewRequest("GET", cst.URL+"/index.html", nil) 1393 if err != nil { 1394 t.Fatal(err) 1395 } 1396 req.Header.Set("Range", tt.r) 1397 res, err := cst.Client().Do(req) 1398 if err != nil { 1399 t.Fatal(err) 1400 } 1401 if g, w := res.StatusCode, tt.wantCode; g != w { 1402 t.Errorf("StatusCode mismatch: got %d want %d", g, w) 1403 } 1404 slurp, err := io.ReadAll(res.Body) 1405 res.Body.Close() 1406 if err != nil { 1407 t.Fatal(err) 1408 } 1409 if g, w := string(slurp), tt.wantBody; g != w { 1410 t.Fatalf("Content mismatch:\nGot: %q\nWant: %q", g, w) 1411 } 1412 }) 1413 } 1414 }