You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

public.go 3.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. // Copyright 2016 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package public
  5. import (
  6. "encoding/base64"
  7. "log"
  8. "net/http"
  9. "path"
  10. "path/filepath"
  11. "strings"
  12. "time"
  13. "code.gitea.io/gitea/modules/setting"
  14. "gitea.com/macaron/macaron"
  15. )
  16. // Options represents the available options to configure the macaron handler.
  17. type Options struct {
  18. Directory string
  19. IndexFile string
  20. SkipLogging bool
  21. // if set to true, will enable caching. Expires header will also be set to
  22. // expire after the defined time.
  23. ExpiresAfter time.Duration
  24. FileSystem http.FileSystem
  25. Prefix string
  26. }
  27. // Custom implements the macaron static handler for serving custom assets.
  28. func Custom(opts *Options) macaron.Handler {
  29. return opts.staticHandler(path.Join(setting.CustomPath, "public"))
  30. }
  31. // staticFileSystem implements http.FileSystem interface.
  32. type staticFileSystem struct {
  33. dir *http.Dir
  34. }
  35. func newStaticFileSystem(directory string) staticFileSystem {
  36. if !filepath.IsAbs(directory) {
  37. directory = filepath.Join(macaron.Root, directory)
  38. }
  39. dir := http.Dir(directory)
  40. return staticFileSystem{&dir}
  41. }
  42. func (fs staticFileSystem) Open(name string) (http.File, error) {
  43. return fs.dir.Open(name)
  44. }
  45. // StaticHandler sets up a new middleware for serving static files in the
  46. func StaticHandler(dir string, opts *Options) macaron.Handler {
  47. return opts.staticHandler(dir)
  48. }
  49. func (opts *Options) staticHandler(dir string) macaron.Handler {
  50. // Defaults
  51. if len(opts.IndexFile) == 0 {
  52. opts.IndexFile = "index.html"
  53. }
  54. // Normalize the prefix if provided
  55. if opts.Prefix != "" {
  56. // Ensure we have a leading '/'
  57. if opts.Prefix[0] != '/' {
  58. opts.Prefix = "/" + opts.Prefix
  59. }
  60. // Remove any trailing '/'
  61. opts.Prefix = strings.TrimRight(opts.Prefix, "/")
  62. }
  63. if opts.FileSystem == nil {
  64. opts.FileSystem = newStaticFileSystem(dir)
  65. }
  66. return func(ctx *macaron.Context, log *log.Logger) {
  67. opts.handle(ctx, log, opts)
  68. }
  69. }
  70. func (opts *Options) handle(ctx *macaron.Context, log *log.Logger, opt *Options) bool {
  71. if ctx.Req.Method != "GET" && ctx.Req.Method != "HEAD" {
  72. return false
  73. }
  74. file := ctx.Req.URL.Path
  75. // if we have a prefix, filter requests by stripping the prefix
  76. if opt.Prefix != "" {
  77. if !strings.HasPrefix(file, opt.Prefix) {
  78. return false
  79. }
  80. file = file[len(opt.Prefix):]
  81. if file != "" && file[0] != '/' {
  82. return false
  83. }
  84. }
  85. f, err := opt.FileSystem.Open(file)
  86. if err != nil {
  87. return false
  88. }
  89. defer f.Close()
  90. fi, err := f.Stat()
  91. if err != nil {
  92. log.Printf("[Static] %q exists, but fails to open: %v", file, err)
  93. return true
  94. }
  95. // Try to serve index file
  96. if fi.IsDir() {
  97. // Redirect if missing trailing slash.
  98. if !strings.HasSuffix(ctx.Req.URL.Path, "/") {
  99. http.Redirect(ctx.Resp, ctx.Req.Request, path.Clean(ctx.Req.URL.Path+"/"), http.StatusFound)
  100. return true
  101. }
  102. f, err = opt.FileSystem.Open(file)
  103. if err != nil {
  104. return false // Discard error.
  105. }
  106. defer f.Close()
  107. fi, err = f.Stat()
  108. if err != nil || fi.IsDir() {
  109. return true
  110. }
  111. }
  112. if !opt.SkipLogging {
  113. log.Println("[Static] Serving " + file)
  114. }
  115. // Add an Expires header to the static content
  116. if opt.ExpiresAfter > 0 {
  117. ctx.Resp.Header().Set("Expires", time.Now().Add(opt.ExpiresAfter).UTC().Format(http.TimeFormat))
  118. tag := GenerateETag(string(fi.Size()), fi.Name(), fi.ModTime().UTC().Format(http.TimeFormat))
  119. ctx.Resp.Header().Set("ETag", tag)
  120. if ctx.Req.Header.Get("If-None-Match") == tag {
  121. ctx.Resp.WriteHeader(304)
  122. return false
  123. }
  124. }
  125. http.ServeContent(ctx.Resp, ctx.Req.Request, file, fi.ModTime(), f)
  126. return true
  127. }
  128. // GenerateETag generates an ETag based on size, filename and file modification time
  129. func GenerateETag(fileSize, fileName, modTime string) string {
  130. etag := fileSize + fileName + modTime
  131. return base64.StdEncoding.EncodeToString([]byte(etag))
  132. }