github.com/cloudwego/hertz@v0.9.3/pkg/app/fs.go (about) 1 /* 2 * Copyright 2022 CloudWeGo Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 * The MIT License (MIT) 17 * 18 * Copyright (c) 2015-present Aliaksandr Valialkin, VertaMedia, Kirill Danshin, Erik Dubbelboer, FastHTTP Authors 19 * 20 * Permission is hereby granted, free of charge, to any person obtaining a copy 21 * of this software and associated documentation files (the "Software"), to deal 22 * in the Software without restriction, including without limitation the rights 23 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 24 * copies of the Software, and to permit persons to whom the Software is 25 * furnished to do so, subject to the following conditions: 26 * 27 * The above copyright notice and this permission notice shall be included in 28 * all copies or substantial portions of the Software. 29 * 30 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 31 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 32 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 33 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 34 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 35 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 36 * THE SOFTWARE. 37 * 38 * This file may have been modified by CloudWeGo authors. All CloudWeGo 39 * Modifications are Copyright 2022 CloudWeGo Authors. 40 */ 41 42 package app 43 44 import ( 45 "bytes" 46 "compress/gzip" 47 "context" 48 "fmt" 49 "html" 50 "io" 51 "io/ioutil" 52 "mime" 53 "net/http" 54 "os" 55 "path/filepath" 56 "sort" 57 "strings" 58 "sync" 59 "time" 60 61 "github.com/cloudwego/hertz/internal/bytesconv" 62 "github.com/cloudwego/hertz/internal/bytestr" 63 "github.com/cloudwego/hertz/internal/nocopy" 64 "github.com/cloudwego/hertz/pkg/common/bytebufferpool" 65 "github.com/cloudwego/hertz/pkg/common/compress" 66 "github.com/cloudwego/hertz/pkg/common/errors" 67 "github.com/cloudwego/hertz/pkg/common/hlog" 68 "github.com/cloudwego/hertz/pkg/common/utils" 69 "github.com/cloudwego/hertz/pkg/network" 70 "github.com/cloudwego/hertz/pkg/protocol" 71 "github.com/cloudwego/hertz/pkg/protocol/consts" 72 ) 73 74 var ( 75 errDirIndexRequired = errors.NewPublic("directory index required") 76 errNoCreatePermission = errors.NewPublic("no 'create file' permissions") 77 78 rootFSOnce sync.Once 79 rootFS = &FS{ 80 Root: "/", 81 GenerateIndexPages: true, 82 Compress: true, 83 AcceptByteRange: true, 84 } 85 rootFSHandler HandlerFunc 86 strInvalidHost = []byte("invalid-host") 87 ) 88 89 // PathRewriteFunc must return new request path based on arbitrary ctx 90 // info such as ctx.Path(). 91 // 92 // Path rewriter is used in FS for translating the current request 93 // to the local filesystem path relative to FS.Root. 94 // 95 // The returned path must not contain '/../' substrings due to security reasons, 96 // since such paths may refer files outside FS.Root. 97 // 98 // The returned path may refer to ctx members. For example, ctx.Path(). 99 type PathRewriteFunc func(ctx *RequestContext) []byte 100 101 // FS represents settings for request handler serving static files 102 // from the local filesystem. 103 // 104 // It is prohibited copying FS values. Create new values instead. 105 type FS struct { 106 noCopy nocopy.NoCopy //lint:ignore U1000 until noCopy is used 107 108 // Path to the root directory to serve files from. 109 Root string 110 111 // List of index file names to try opening during directory access. 112 // 113 // For example: 114 // 115 // * index.html 116 // * index.htm 117 // * my-super-index.xml 118 // 119 // By default the list is empty. 120 IndexNames []string 121 122 // Index pages for directories without files matching IndexNames 123 // are automatically generated if set. 124 // 125 // Directory index generation may be quite slow for directories 126 // with many files (more than 1K), so it is discouraged enabling 127 // index pages' generation for such directories. 128 // 129 // By default index pages aren't generated. 130 GenerateIndexPages bool 131 132 // Transparently compresses responses if set to true. 133 // 134 // The server tries minimizing CPU usage by caching compressed files. 135 // It adds CompressedFileSuffix suffix to the original file name and 136 // tries saving the resulting compressed file under the new file name. 137 // So it is advisable to give the server write access to Root 138 // and to all inner folders in order to minimize CPU usage when serving 139 // compressed responses. 140 // 141 // Transparent compression is disabled by default. 142 Compress bool 143 144 // Enables byte range requests if set to true. 145 // 146 // Byte range requests are disabled by default. 147 AcceptByteRange bool 148 149 // Path rewriting function. 150 // 151 // By default request path is not modified. 152 PathRewrite PathRewriteFunc 153 154 // PathNotFound fires when file is not found in filesystem 155 // this functions tries to replace "Cannot open requested path" 156 // server response giving to the programmer the control of server flow. 157 // 158 // By default PathNotFound returns 159 // "Cannot open requested path" 160 PathNotFound HandlerFunc 161 162 // Expiration duration for inactive file handlers. 163 // 164 // FSHandlerCacheDuration is used by default. 165 CacheDuration time.Duration 166 167 // Suffix to add to the name of cached compressed file. 168 // 169 // This value has sense only if Compress is set. 170 // 171 // FSCompressedFileSuffix is used by default. 172 CompressedFileSuffix string 173 174 once sync.Once 175 h HandlerFunc 176 } 177 178 type byteRangeUpdater interface { 179 UpdateByteRange(startPos, endPos int) error 180 } 181 182 type fsSmallFileReader struct { 183 ff *fsFile 184 startPos int 185 endPos int 186 } 187 188 func (r *fsSmallFileReader) Close() error { 189 ff := r.ff 190 ff.decReadersCount() 191 r.ff = nil 192 r.startPos = 0 193 r.endPos = 0 194 ff.h.smallFileReaderPool.Put(r) 195 return nil 196 } 197 198 func (r *fsSmallFileReader) UpdateByteRange(startPos, endPos int) error { 199 r.startPos = startPos 200 r.endPos = endPos + 1 201 return nil 202 } 203 204 func (r *fsSmallFileReader) Read(p []byte) (int, error) { 205 tailLen := r.endPos - r.startPos 206 if tailLen <= 0 { 207 return 0, io.EOF 208 } 209 if len(p) > tailLen { 210 p = p[:tailLen] 211 } 212 213 ff := r.ff 214 if ff.f != nil { 215 n, err := ff.f.ReadAt(p, int64(r.startPos)) 216 r.startPos += n 217 return n, err 218 } 219 220 n := copy(p, ff.dirIndex[r.startPos:]) 221 r.startPos += n 222 return n, nil 223 } 224 225 func (r *fsSmallFileReader) WriteTo(w io.Writer) (int64, error) { 226 ff := r.ff 227 228 var n int 229 var err error 230 if ff.f == nil { 231 n, err = w.Write(ff.dirIndex[r.startPos:r.endPos]) 232 return int64(n), err 233 } 234 235 if rf, ok := w.(io.ReaderFrom); ok { 236 return rf.ReadFrom(r) 237 } 238 239 curPos := r.startPos 240 bufv := utils.CopyBufPool.Get() 241 buf := bufv.([]byte) 242 for err == nil { 243 tailLen := r.endPos - curPos 244 if tailLen <= 0 { 245 break 246 } 247 if len(buf) > tailLen { 248 buf = buf[:tailLen] 249 } 250 n, err = ff.f.ReadAt(buf, int64(curPos)) 251 nw, errw := w.Write(buf[:n]) 252 curPos += nw 253 if errw == nil && nw != n { 254 panic("BUG: Write(p) returned (n, nil), where n != len(p)") 255 } 256 if err == nil { 257 err = errw 258 } 259 } 260 utils.CopyBufPool.Put(bufv) 261 262 if err == io.EOF { 263 err = nil 264 } 265 return int64(curPos - r.startPos), err 266 } 267 268 // ServeFile returns HTTP response containing compressed file contents 269 // from the given path. 270 // 271 // HTTP response may contain uncompressed file contents in the following cases: 272 // 273 // - Missing 'Accept-Encoding: gzip' request header. 274 // - No write access to directory containing the file. 275 // 276 // Directory contents is returned if path points to directory. 277 // 278 // Use ServeFileUncompressed is you don't need serving compressed file contents. 279 func ServeFile(ctx *RequestContext, path string) { 280 rootFSOnce.Do(func() { 281 rootFSHandler = rootFS.NewRequestHandler() 282 }) 283 if len(path) == 0 || path[0] != '/' { 284 // extend relative path to absolute path 285 var err error 286 if path, err = filepath.Abs(path); err != nil { 287 hlog.SystemLogger().Errorf("Cannot resolve path=%q to absolute file error=%s", path, err) 288 ctx.AbortWithMsg("Internal Server Error", consts.StatusInternalServerError) 289 return 290 } 291 } 292 ctx.Request.SetRequestURI(path) 293 rootFSHandler(context.Background(), ctx) 294 } 295 296 // NewRequestHandler returns new request handler with the given FS settings. 297 // 298 // The returned handler caches requested file handles 299 // for FS.CacheDuration. 300 // Make sure your program has enough 'max open files' limit aka 301 // 'ulimit -n' if FS.Root folder contains many files. 302 // 303 // Do not create multiple request handlers from a single FS instance - 304 // just reuse a single request handler. 305 func (fs *FS) NewRequestHandler() HandlerFunc { 306 fs.once.Do(fs.initRequestHandler) 307 return fs.h 308 } 309 310 func (fs *FS) initRequestHandler() { 311 root := fs.Root 312 313 // serve files from the current working directory if root is empty 314 if len(root) == 0 { 315 root = "." 316 } 317 318 // strip trailing slashes from the root path 319 for len(root) > 0 && root[len(root)-1] == '/' { 320 root = root[:len(root)-1] 321 } 322 323 cacheDuration := fs.CacheDuration 324 if cacheDuration <= 0 { 325 cacheDuration = consts.FSHandlerCacheDuration 326 } 327 compressedFileSuffix := fs.CompressedFileSuffix 328 if len(compressedFileSuffix) == 0 { 329 compressedFileSuffix = consts.FSCompressedFileSuffix 330 } 331 332 h := &fsHandler{ 333 root: root, 334 indexNames: fs.IndexNames, 335 pathRewrite: fs.PathRewrite, 336 generateIndexPages: fs.GenerateIndexPages, 337 compress: fs.Compress, 338 pathNotFound: fs.PathNotFound, 339 acceptByteRange: fs.AcceptByteRange, 340 cacheDuration: cacheDuration, 341 compressedFileSuffix: compressedFileSuffix, 342 cache: make(map[string]*fsFile), 343 compressedCache: make(map[string]*fsFile), 344 } 345 346 go func() { 347 var pendingFiles []*fsFile 348 for { 349 time.Sleep(cacheDuration / 2) 350 pendingFiles = h.cleanCache(pendingFiles) 351 } 352 }() 353 354 fs.h = h.handleRequest 355 } 356 357 type fsHandler struct { 358 root string 359 indexNames []string 360 pathRewrite PathRewriteFunc 361 pathNotFound HandlerFunc 362 generateIndexPages bool 363 compress bool 364 acceptByteRange bool 365 cacheDuration time.Duration 366 compressedFileSuffix string 367 368 cache map[string]*fsFile 369 compressedCache map[string]*fsFile 370 cacheLock sync.Mutex 371 372 smallFileReaderPool sync.Pool 373 } 374 375 // bigFileReader attempts to trigger sendfile 376 // for sending big files over the wire. 377 type bigFileReader struct { 378 f *os.File 379 ff *fsFile 380 r io.Reader 381 lr io.LimitedReader 382 } 383 384 func (r *bigFileReader) UpdateByteRange(startPos, endPos int) error { 385 if _, err := r.f.Seek(int64(startPos), 0); err != nil { 386 return err 387 } 388 r.r = &r.lr 389 r.lr.R = r.f 390 r.lr.N = int64(endPos - startPos + 1) 391 return nil 392 } 393 394 func (r *bigFileReader) Read(p []byte) (int, error) { 395 return r.r.Read(p) 396 } 397 398 func (r *bigFileReader) WriteTo(w io.Writer) (int64, error) { 399 if rf, ok := w.(io.ReaderFrom); ok { 400 // fast path. Sendfile must be triggered 401 return rf.ReadFrom(r.r) 402 } 403 zw := network.NewWriter(w) 404 // slow pathw 405 return utils.CopyZeroAlloc(zw, r.r) 406 } 407 408 func (r *bigFileReader) Close() error { 409 r.r = r.f 410 n, err := r.f.Seek(0, 0) 411 if err == nil { 412 if n != 0 { 413 panic("BUG: File.Seek(0,0) returned (non-zero, nil)") 414 } 415 416 ff := r.ff 417 ff.bigFilesLock.Lock() 418 ff.bigFiles = append(ff.bigFiles, r) 419 ff.bigFilesLock.Unlock() 420 } else { 421 r.f.Close() 422 } 423 r.ff.decReadersCount() 424 return err 425 } 426 427 func (h *fsHandler) cleanCache(pendingFiles []*fsFile) []*fsFile { 428 var filesToRelease []*fsFile 429 430 h.cacheLock.Lock() 431 432 // Close files which couldn't be closed before due to non-zero 433 // readers count on the previous run. 434 var remainingFiles []*fsFile 435 for _, ff := range pendingFiles { 436 if ff.readersCount > 0 { 437 remainingFiles = append(remainingFiles, ff) 438 } else { 439 filesToRelease = append(filesToRelease, ff) 440 } 441 } 442 pendingFiles = remainingFiles 443 444 pendingFiles, filesToRelease = cleanCacheNolock(h.cache, pendingFiles, filesToRelease, h.cacheDuration) 445 pendingFiles, filesToRelease = cleanCacheNolock(h.compressedCache, pendingFiles, filesToRelease, h.cacheDuration) 446 447 h.cacheLock.Unlock() 448 449 for _, ff := range filesToRelease { 450 ff.Release() 451 } 452 453 return pendingFiles 454 } 455 456 func (h *fsHandler) compressAndOpenFSFile(filePath string) (*fsFile, error) { 457 f, err := os.Open(filePath) 458 if err != nil { 459 return nil, err 460 } 461 462 fileInfo, err := f.Stat() 463 if err != nil { 464 f.Close() 465 return nil, fmt.Errorf("cannot obtain info for file %q: %s", filePath, err) 466 } 467 468 if fileInfo.IsDir() { 469 f.Close() 470 return nil, errDirIndexRequired 471 } 472 473 if strings.HasSuffix(filePath, h.compressedFileSuffix) || 474 fileInfo.Size() > consts.FsMaxCompressibleFileSize || 475 !isFileCompressible(f, consts.FsMinCompressRatio) { 476 return h.newFSFile(f, fileInfo, false) 477 } 478 479 compressedFilePath := filePath + h.compressedFileSuffix 480 absPath, err := filepath.Abs(compressedFilePath) 481 if err != nil { 482 f.Close() 483 return nil, fmt.Errorf("cannot determine absolute path for %q: %s", compressedFilePath, err) 484 } 485 486 flock := getFileLock(absPath) 487 flock.Lock() 488 ff, err := h.compressFileNolock(f, fileInfo, filePath, compressedFilePath) 489 flock.Unlock() 490 491 return ff, err 492 } 493 494 func (h *fsHandler) newCompressedFSFile(filePath string) (*fsFile, error) { 495 f, err := os.Open(filePath) 496 if err != nil { 497 return nil, fmt.Errorf("cannot open compressed file %q: %s", filePath, err) 498 } 499 fileInfo, err := f.Stat() 500 if err != nil { 501 f.Close() 502 return nil, fmt.Errorf("cannot obtain info for compressed file %q: %s", filePath, err) 503 } 504 return h.newFSFile(f, fileInfo, true) 505 } 506 507 func (h *fsHandler) compressFileNolock(f *os.File, fileInfo os.FileInfo, filePath, compressedFilePath string) (*fsFile, error) { 508 // Attempt to open compressed file created by another concurrent 509 // goroutine. 510 // It is safe opening such a file, since the file creation 511 // is guarded by file mutex - see getFileLock call. 512 if _, err := os.Stat(compressedFilePath); err == nil { 513 f.Close() 514 return h.newCompressedFSFile(compressedFilePath) 515 } 516 517 // Create temporary file, so concurrent goroutines don't use 518 // it until it is created. 519 tmpFilePath := compressedFilePath + ".tmp" 520 zf, err := os.Create(tmpFilePath) 521 if err != nil { 522 f.Close() 523 if !os.IsPermission(err) { 524 return nil, fmt.Errorf("cannot create temporary file %q: %s", tmpFilePath, err) 525 } 526 return nil, errNoCreatePermission 527 } 528 529 zw := compress.AcquireStacklessGzipWriter(zf, compress.CompressDefaultCompression) 530 zrw := network.NewWriter(zw) 531 _, err = utils.CopyZeroAlloc(zrw, f) 532 if err1 := zw.Flush(); err == nil { 533 err = err1 534 } 535 compress.ReleaseStacklessGzipWriter(zw, compress.CompressDefaultCompression) 536 zf.Close() 537 f.Close() 538 if err != nil { 539 return nil, fmt.Errorf("error when compressing file %q to %q: %s", filePath, tmpFilePath, err) 540 } 541 if err = os.Chtimes(tmpFilePath, time.Now(), fileInfo.ModTime()); err != nil { 542 return nil, fmt.Errorf("cannot change modification time to %s for tmp file %q: %s", 543 fileInfo.ModTime(), tmpFilePath, err) 544 } 545 if err = os.Rename(tmpFilePath, compressedFilePath); err != nil { 546 return nil, fmt.Errorf("cannot move compressed file from %q to %q: %s", tmpFilePath, compressedFilePath, err) 547 } 548 return h.newCompressedFSFile(compressedFilePath) 549 } 550 551 func (h *fsHandler) openFSFile(filePath string, mustCompress bool) (*fsFile, error) { 552 filePathOriginal := filePath 553 if mustCompress { 554 filePath += h.compressedFileSuffix 555 } 556 557 f, err := os.Open(filePath) 558 if err != nil { 559 if mustCompress && os.IsNotExist(err) { 560 return h.compressAndOpenFSFile(filePathOriginal) 561 } 562 return nil, err 563 } 564 565 fileInfo, err := f.Stat() 566 if err != nil { 567 f.Close() 568 return nil, fmt.Errorf("cannot obtain info for file %q: %s", filePath, err) 569 } 570 571 if fileInfo.IsDir() { 572 f.Close() 573 if mustCompress { 574 return nil, fmt.Errorf("directory with unexpected suffix found: %q. Suffix: %q", 575 filePath, h.compressedFileSuffix) 576 } 577 return nil, errDirIndexRequired 578 } 579 580 if mustCompress { 581 fileInfoOriginal, err := os.Stat(filePathOriginal) 582 if err != nil { 583 f.Close() 584 return nil, fmt.Errorf("cannot obtain info for original file %q: %s", filePathOriginal, err) 585 } 586 587 if fileInfoOriginal.ModTime() != fileInfo.ModTime() { 588 // The compressed file became stale. Re-create it. 589 f.Close() 590 os.Remove(filePath) 591 return h.compressAndOpenFSFile(filePathOriginal) 592 } 593 } 594 595 return h.newFSFile(f, fileInfo, mustCompress) 596 } 597 598 func (h *fsHandler) newFSFile(f *os.File, fileInfo os.FileInfo, compressed bool) (*fsFile, error) { 599 n := fileInfo.Size() 600 contentLength := int(n) 601 if n != int64(contentLength) { 602 f.Close() 603 return nil, fmt.Errorf("too big file: %d bytes", n) 604 } 605 606 // detect content-type 607 ext := fileExtension(fileInfo.Name(), compressed, h.compressedFileSuffix) 608 contentType := mime.TypeByExtension(ext) 609 if len(contentType) == 0 { 610 data, err := readFileHeader(f, compressed) 611 if err != nil { 612 return nil, fmt.Errorf("cannot read header of the file %q: %s", f.Name(), err) 613 } 614 contentType = http.DetectContentType(data) 615 } 616 617 lastModified := fileInfo.ModTime() 618 ff := &fsFile{ 619 h: h, 620 f: f, 621 contentType: contentType, 622 contentLength: contentLength, 623 compressed: compressed, 624 lastModified: lastModified, 625 lastModifiedStr: bytesconv.AppendHTTPDate(make([]byte, 0, len(http.TimeFormat)), lastModified), 626 627 t: time.Now(), 628 } 629 return ff, nil 630 } 631 632 func (h *fsHandler) createDirIndex(base *protocol.URI, dirPath string, mustCompress bool) (*fsFile, error) { 633 w := &bytebufferpool.ByteBuffer{} 634 635 basePathEscaped := html.EscapeString(string(base.Path())) 636 fmt.Fprintf(w, "<html><head><title>%s</title><style>.dir { font-weight: bold }</style></head><body>", basePathEscaped) 637 fmt.Fprintf(w, "<h1>%s</h1>", basePathEscaped) 638 fmt.Fprintf(w, "<ul>") 639 640 if len(basePathEscaped) > 1 { 641 var parentURI protocol.URI 642 base.CopyTo(&parentURI) 643 parentURI.Update(string(base.Path()) + "/..") 644 parentPathEscaped := html.EscapeString(string(parentURI.Path())) 645 fmt.Fprintf(w, `<li><a href="%s" class="dir">..</a></li>`, parentPathEscaped) 646 } 647 648 f, err := os.Open(dirPath) 649 if err != nil { 650 return nil, err 651 } 652 653 fileinfos, err := f.Readdir(0) 654 f.Close() 655 if err != nil { 656 return nil, err 657 } 658 659 fm := make(map[string]os.FileInfo, len(fileinfos)) 660 filenames := make([]string, 0, len(fileinfos)) 661 for _, fi := range fileinfos { 662 name := fi.Name() 663 if strings.HasSuffix(name, h.compressedFileSuffix) { 664 // Do not show compressed files on index page. 665 continue 666 } 667 fm[name] = fi 668 filenames = append(filenames, name) 669 } 670 671 var u protocol.URI 672 base.CopyTo(&u) 673 u.Update(string(u.Path()) + "/") 674 675 sort.Strings(filenames) 676 for _, name := range filenames { 677 u.Update(name) 678 pathEscaped := html.EscapeString(string(u.Path())) 679 fi := fm[name] 680 auxStr := "dir" 681 className := "dir" 682 if !fi.IsDir() { 683 auxStr = fmt.Sprintf("file, %d bytes", fi.Size()) 684 className = "file" 685 } 686 fmt.Fprintf(w, `<li><a href="%s" class="%s">%s</a>, %s, last modified %s</li>`, 687 pathEscaped, className, html.EscapeString(name), auxStr, fsModTime(fi.ModTime())) 688 } 689 690 fmt.Fprintf(w, "</ul></body></html>") 691 if mustCompress { 692 var zbuf bytebufferpool.ByteBuffer 693 zbuf.B = compress.AppendGzipBytesLevel(zbuf.B, w.B, compress.CompressDefaultCompression) 694 w = &zbuf 695 } 696 697 dirIndex := w.B 698 lastModified := time.Now() 699 ff := &fsFile{ 700 h: h, 701 dirIndex: dirIndex, 702 contentType: "text/html; charset=utf-8", 703 contentLength: len(dirIndex), 704 compressed: mustCompress, 705 lastModified: lastModified, 706 lastModifiedStr: bytesconv.AppendHTTPDate(make([]byte, 0, len(http.TimeFormat)), lastModified), 707 708 t: lastModified, 709 } 710 return ff, nil 711 } 712 713 func (h *fsHandler) openIndexFile(ctx *RequestContext, dirPath string, mustCompress bool) (*fsFile, error) { 714 for _, indexName := range h.indexNames { 715 indexFilePath := dirPath + "/" + indexName 716 ff, err := h.openFSFile(indexFilePath, mustCompress) 717 if err == nil { 718 return ff, nil 719 } 720 if !os.IsNotExist(err) { 721 return nil, fmt.Errorf("cannot open file %q: %s", indexFilePath, err) 722 } 723 } 724 725 if !h.generateIndexPages { 726 return nil, fmt.Errorf("cannot access directory without index page. Directory %q", dirPath) 727 } 728 729 return h.createDirIndex(ctx.URI(), dirPath, mustCompress) 730 } 731 732 func (ff *fsFile) decReadersCount() { 733 ff.h.cacheLock.Lock() 734 defer ff.h.cacheLock.Unlock() 735 ff.readersCount-- 736 if ff.readersCount < 0 { 737 panic("BUG: negative fsFile.readersCount!") 738 } 739 } 740 741 func (ff *fsFile) bigFileReader() (io.Reader, error) { 742 if ff.f == nil { 743 panic("BUG: ff.f must be non-nil in bigFileReader") 744 } 745 746 var r io.Reader 747 748 ff.bigFilesLock.Lock() 749 n := len(ff.bigFiles) 750 if n > 0 { 751 r = ff.bigFiles[n-1] 752 ff.bigFiles = ff.bigFiles[:n-1] 753 } 754 ff.bigFilesLock.Unlock() 755 756 if r != nil { 757 return r, nil 758 } 759 760 f, err := os.Open(ff.f.Name()) 761 if err != nil { 762 return nil, fmt.Errorf("cannot open already opened file: %s", err) 763 } 764 return &bigFileReader{ 765 f: f, 766 ff: ff, 767 r: f, 768 }, nil 769 } 770 771 func (ff *fsFile) NewReader() (io.Reader, error) { 772 if ff.isBig() { 773 r, err := ff.bigFileReader() 774 if err != nil { 775 ff.decReadersCount() 776 } 777 return r, err 778 } 779 return ff.smallFileReader(), nil 780 } 781 782 func (ff *fsFile) smallFileReader() io.Reader { 783 v := ff.h.smallFileReaderPool.Get() 784 if v == nil { 785 v = &fsSmallFileReader{} 786 } 787 r := v.(*fsSmallFileReader) 788 r.ff = ff 789 r.endPos = ff.contentLength 790 if r.startPos > 0 { 791 panic("BUG: fsSmallFileReader with non-nil startPos found in the pool") 792 } 793 return r 794 } 795 796 func (h *fsHandler) handleRequest(c context.Context, ctx *RequestContext) { 797 var path []byte 798 if h.pathRewrite != nil { 799 path = h.pathRewrite(ctx) 800 } else { 801 path = ctx.Path() 802 } 803 path = stripTrailingSlashes(path) 804 805 if n := bytes.IndexByte(path, 0); n >= 0 { 806 hlog.SystemLogger().Errorf("Cannot serve path with nil byte at position=%d, path=%q", n, path) 807 ctx.AbortWithMsg("Are you a hacker?", consts.StatusBadRequest) 808 return 809 } 810 if h.pathRewrite != nil { 811 // There is no need to check for '/../' if path = ctx.Path(), 812 // since ctx.Path must normalize and sanitize the path. 813 814 if n := bytes.Index(path, bytestr.StrSlashDotDotSlash); n >= 0 { 815 hlog.SystemLogger().Errorf("Cannot serve path with '/../' at position=%d due to security reasons, path=%q", n, path) 816 ctx.AbortWithMsg("Internal Server Error", consts.StatusInternalServerError) 817 return 818 } 819 } 820 821 mustCompress := false 822 fileCache := h.cache 823 byteRange := ctx.Request.Header.PeekRange() 824 if len(byteRange) == 0 && h.compress && ctx.Request.Header.HasAcceptEncodingBytes(bytestr.StrGzip) { 825 mustCompress = true 826 fileCache = h.compressedCache 827 } 828 829 h.cacheLock.Lock() 830 ff, ok := fileCache[string(path)] 831 if ok { 832 ff.readersCount++ 833 } 834 h.cacheLock.Unlock() 835 836 if !ok { 837 pathStr := string(path) 838 filePath := h.root + pathStr 839 var err error 840 ff, err = h.openFSFile(filePath, mustCompress) 841 842 if mustCompress && err == errNoCreatePermission { 843 hlog.SystemLogger().Errorf("Insufficient permissions for saving compressed file for path=%q. Serving uncompressed file. "+ 844 "Allow write access to the directory with this file in order to improve hertz performance", filePath) 845 mustCompress = false 846 ff, err = h.openFSFile(filePath, mustCompress) 847 } 848 if err == errDirIndexRequired { 849 ff, err = h.openIndexFile(ctx, filePath, mustCompress) 850 if err != nil { 851 hlog.SystemLogger().Errorf("Cannot open dir index, path=%q, error=%s", filePath, err) 852 ctx.AbortWithMsg("Directory index is forbidden", consts.StatusForbidden) 853 return 854 } 855 } else if err != nil { 856 hlog.SystemLogger().Errorf("Cannot open file=%q, error=%s", filePath, err) 857 if h.pathNotFound == nil { 858 ctx.AbortWithMsg("Cannot open requested path", consts.StatusNotFound) 859 } else { 860 ctx.SetStatusCode(consts.StatusNotFound) 861 h.pathNotFound(c, ctx) 862 } 863 return 864 } 865 866 h.cacheLock.Lock() 867 ff1, ok := fileCache[pathStr] 868 if !ok { 869 fileCache[pathStr] = ff 870 ff.readersCount++ 871 } else { 872 ff1.readersCount++ 873 } 874 h.cacheLock.Unlock() 875 876 if ok { 877 // The file has been already opened by another 878 // goroutine, so close the current file and use 879 // the file opened by another goroutine instead. 880 ff.Release() 881 ff = ff1 882 } 883 } 884 885 if !ctx.IfModifiedSince(ff.lastModified) { 886 ff.decReadersCount() 887 ctx.NotModified() 888 return 889 } 890 891 r, err := ff.NewReader() 892 if err != nil { 893 hlog.SystemLogger().Errorf("Cannot obtain file reader for path=%q, error=%s", path, err) 894 ctx.AbortWithMsg("Internal Server Error", consts.StatusInternalServerError) 895 return 896 } 897 898 hdr := &ctx.Response.Header 899 if ff.compressed { 900 hdr.SetContentEncodingBytes(bytestr.StrGzip) 901 } 902 903 statusCode := consts.StatusOK 904 contentLength := ff.contentLength 905 if h.acceptByteRange { 906 hdr.SetCanonical(bytestr.StrAcceptRanges, bytestr.StrBytes) 907 if len(byteRange) > 0 { 908 startPos, endPos, err := ParseByteRange(byteRange, contentLength) 909 if err != nil { 910 r.(io.Closer).Close() 911 hlog.SystemLogger().Errorf("Cannot parse byte range %q for path=%q,error=%s", byteRange, path, err) 912 ctx.AbortWithMsg("Range Not Satisfiable", consts.StatusRequestedRangeNotSatisfiable) 913 return 914 } 915 916 if err = r.(byteRangeUpdater).UpdateByteRange(startPos, endPos); err != nil { 917 r.(io.Closer).Close() 918 hlog.SystemLogger().Errorf("Cannot seek byte range %q for path=%q, error=%s", byteRange, path, err) 919 ctx.AbortWithMsg("Internal Server Error", consts.StatusInternalServerError) 920 return 921 } 922 923 hdr.SetContentRange(startPos, endPos, contentLength) 924 contentLength = endPos - startPos + 1 925 statusCode = consts.StatusPartialContent 926 } 927 } 928 929 hdr.SetCanonical(bytestr.StrLastModified, ff.lastModifiedStr) 930 if !ctx.IsHead() { 931 ctx.SetBodyStream(r, contentLength) 932 } else { 933 ctx.Response.ResetBody() 934 ctx.Response.SkipBody = true 935 ctx.Response.Header.SetContentLength(contentLength) 936 if rc, ok := r.(io.Closer); ok { 937 if err := rc.Close(); err != nil { 938 hlog.SystemLogger().Errorf("Cannot close file reader: error=%s", err) 939 ctx.AbortWithMsg("Internal Server Error", consts.StatusInternalServerError) 940 return 941 } 942 } 943 } 944 hdr.SetNoDefaultContentType(true) 945 if len(hdr.ContentType()) == 0 { 946 ctx.SetContentType(ff.contentType) 947 } 948 ctx.SetStatusCode(statusCode) 949 } 950 951 type fsFile struct { 952 h *fsHandler 953 f *os.File 954 dirIndex []byte 955 contentType string 956 contentLength int 957 compressed bool 958 959 lastModified time.Time 960 lastModifiedStr []byte 961 962 t time.Time 963 readersCount int 964 965 bigFiles []*bigFileReader 966 bigFilesLock sync.Mutex 967 } 968 969 func (ff *fsFile) Release() { 970 if ff.f != nil { 971 ff.f.Close() 972 973 if ff.isBig() { 974 ff.bigFilesLock.Lock() 975 for _, r := range ff.bigFiles { 976 r.f.Close() 977 } 978 ff.bigFilesLock.Unlock() 979 } 980 } 981 } 982 983 func (ff *fsFile) isBig() bool { 984 return ff.contentLength > consts.MaxSmallFileSize && len(ff.dirIndex) == 0 985 } 986 987 func cleanCacheNolock(cache map[string]*fsFile, pendingFiles, filesToRelease []*fsFile, cacheDuration time.Duration) ([]*fsFile, []*fsFile) { 988 t := time.Now() 989 for k, ff := range cache { 990 if t.Sub(ff.t) > cacheDuration { 991 if ff.readersCount > 0 { 992 // There are pending readers on stale file handle, 993 // so we cannot close it. Put it into pendingFiles 994 // so it will be closed later. 995 pendingFiles = append(pendingFiles, ff) 996 } else { 997 filesToRelease = append(filesToRelease, ff) 998 } 999 delete(cache, k) 1000 } 1001 } 1002 return pendingFiles, filesToRelease 1003 } 1004 1005 func stripTrailingSlashes(path []byte) []byte { 1006 for len(path) > 0 && path[len(path)-1] == '/' { 1007 path = path[:len(path)-1] 1008 } 1009 return path 1010 } 1011 1012 func isFileCompressible(f *os.File, minCompressRatio float64) bool { 1013 // Try compressing the first 4kb of the file 1014 // and see if it can be compressed by more than 1015 // the given minCompressRatio. 1016 b := bytebufferpool.Get() 1017 zw := compress.AcquireStacklessGzipWriter(b, compress.CompressDefaultCompression) 1018 lr := &io.LimitedReader{ 1019 R: f, 1020 N: 4096, 1021 } 1022 zrw := network.NewWriter(zw) 1023 _, err := utils.CopyZeroAlloc(zrw, lr) 1024 compress.ReleaseStacklessGzipWriter(zw, compress.CompressDefaultCompression) 1025 f.Seek(0, 0) //nolint:errcheck 1026 if err != nil { 1027 return false 1028 } 1029 1030 n := 4096 - lr.N 1031 zn := len(b.B) 1032 bytebufferpool.Put(b) 1033 return float64(zn) < float64(n)*minCompressRatio 1034 } 1035 1036 var ( 1037 filesLockMap = make(map[string]*sync.Mutex) 1038 filesLockMapLock sync.Mutex 1039 ) 1040 1041 func getFileLock(absPath string) *sync.Mutex { 1042 filesLockMapLock.Lock() 1043 flock := filesLockMap[absPath] 1044 if flock == nil { 1045 flock = &sync.Mutex{} 1046 filesLockMap[absPath] = flock 1047 } 1048 filesLockMapLock.Unlock() 1049 return flock 1050 } 1051 1052 func fileExtension(path string, compressed bool, compressedFileSuffix string) string { 1053 if compressed && strings.HasSuffix(path, compressedFileSuffix) { 1054 path = path[:len(path)-len(compressedFileSuffix)] 1055 } 1056 n := strings.LastIndexByte(path, '.') 1057 if n < 0 { 1058 return "" 1059 } 1060 return path[n:] 1061 } 1062 1063 func readFileHeader(f *os.File, compressed bool) ([]byte, error) { 1064 r := io.Reader(f) 1065 var zr *gzip.Reader 1066 if compressed { 1067 var err error 1068 if zr, err = compress.AcquireGzipReader(f); err != nil { 1069 return nil, err 1070 } 1071 r = zr 1072 } 1073 1074 lr := &io.LimitedReader{ 1075 R: r, 1076 N: 512, 1077 } 1078 data, err := ioutil.ReadAll(lr) 1079 if _, err := f.Seek(0, 0); err != nil { 1080 return nil, err 1081 } 1082 1083 if zr != nil { 1084 compress.ReleaseGzipReader(zr) 1085 } 1086 1087 return data, err 1088 } 1089 1090 func fsModTime(t time.Time) time.Time { 1091 return t.In(time.UTC).Truncate(time.Second) 1092 } 1093 1094 // ParseByteRange parses 'Range: bytes=...' header value. 1095 // 1096 // It follows https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35 . 1097 func ParseByteRange(byteRange []byte, contentLength int) (startPos, endPos int, err error) { 1098 b := byteRange 1099 if !bytes.HasPrefix(b, bytestr.StrBytes) { 1100 return 0, 0, fmt.Errorf("unsupported range units: %q. Expecting %q", byteRange, bytestr.StrBytes) 1101 } 1102 1103 b = b[len(bytestr.StrBytes):] 1104 if len(b) == 0 || b[0] != '=' { 1105 return 0, 0, fmt.Errorf("missing byte range in %q", byteRange) 1106 } 1107 b = b[1:] 1108 1109 n := bytes.IndexByte(b, '-') 1110 if n < 0 { 1111 return 0, 0, fmt.Errorf("missing the end position of byte range in %q", byteRange) 1112 } 1113 1114 if n == 0 { 1115 v, err := bytesconv.ParseUint(b[n+1:]) 1116 if err != nil { 1117 return 0, 0, err 1118 } 1119 startPos := contentLength - v 1120 if startPos < 0 { 1121 startPos = 0 1122 } 1123 return startPos, contentLength - 1, nil 1124 } 1125 1126 if startPos, err = bytesconv.ParseUint(b[:n]); err != nil { 1127 return 0, 0, err 1128 } 1129 if startPos >= contentLength { 1130 return 0, 0, fmt.Errorf("the start position of byte range cannot exceed %d. byte range %q", contentLength-1, byteRange) 1131 } 1132 1133 b = b[n+1:] 1134 if len(b) == 0 { 1135 return startPos, contentLength - 1, nil 1136 } 1137 1138 if endPos, err = bytesconv.ParseUint(b); err != nil { 1139 return 0, 0, err 1140 } 1141 if endPos >= contentLength { 1142 endPos = contentLength - 1 1143 } 1144 if endPos < startPos { 1145 return 0, 0, fmt.Errorf("the start position of byte range cannot exceed the end position. byte range %q", byteRange) 1146 } 1147 return startPos, endPos, nil 1148 } 1149 1150 // NewVHostPathRewriter returns path rewriter, which strips slashesCount 1151 // leading slashes from the path and prepends the path with request's host, 1152 // thus simplifying virtual hosting for static files. 1153 // 1154 // Examples: 1155 // 1156 // - host=foobar.com, slashesCount=0, original path="/foo/bar". 1157 // Resulting path: "/foobar.com/foo/bar" 1158 // 1159 // - host=img.aaa.com, slashesCount=1, original path="/images/123/456.jpg" 1160 // Resulting path: "/img.aaa.com/123/456.jpg" 1161 func NewVHostPathRewriter(slashesCount int) PathRewriteFunc { 1162 return func(ctx *RequestContext) []byte { 1163 path := stripLeadingSlashes(ctx.Path(), slashesCount) 1164 host := ctx.Host() 1165 if n := bytes.IndexByte(host, '/'); n >= 0 { 1166 host = nil 1167 } 1168 if len(host) == 0 { 1169 host = strInvalidHost 1170 } 1171 b := bytebufferpool.Get() 1172 b.B = append(b.B, '/') 1173 b.B = append(b.B, host...) 1174 b.B = append(b.B, path...) 1175 ctx.URI().SetPathBytes(b.B) 1176 bytebufferpool.Put(b) 1177 1178 return ctx.Path() 1179 } 1180 } 1181 1182 func stripLeadingSlashes(path []byte, stripSlashes int) []byte { 1183 for stripSlashes > 0 && len(path) > 0 { 1184 if path[0] != '/' { 1185 panic("BUG: path must start with slash") 1186 } 1187 n := bytes.IndexByte(path[1:], '/') 1188 if n < 0 { 1189 path = path[:0] 1190 break 1191 } 1192 path = path[n+1:] 1193 stripSlashes-- 1194 } 1195 return path 1196 } 1197 1198 // ServeFileUncompressed returns HTTP response containing file contents 1199 // from the given path. 1200 // 1201 // Directory contents is returned if path points to directory. 1202 // 1203 // ServeFile may be used for saving network traffic when serving files 1204 // with good compression ratio. 1205 func ServeFileUncompressed(ctx *RequestContext, path string) { 1206 ctx.Request.Header.DelBytes(bytestr.StrAcceptEncoding) 1207 ServeFile(ctx, path) 1208 } 1209 1210 // NewPathSlashesStripper returns path rewriter, which strips slashesCount 1211 // leading slashes from the path. 1212 // 1213 // Examples: 1214 // 1215 // - slashesCount = 0, original path: "/foo/bar", result: "/foo/bar" 1216 // - slashesCount = 1, original path: "/foo/bar", result: "/bar" 1217 // - slashesCount = 2, original path: "/foo/bar", result: "" 1218 // 1219 // The returned path rewriter may be used as FS.PathRewrite . 1220 func NewPathSlashesStripper(slashesCount int) PathRewriteFunc { 1221 return func(ctx *RequestContext) []byte { 1222 return stripLeadingSlashes(ctx.Path(), slashesCount) 1223 } 1224 }