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 4.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  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. // List of known entries inside the `public` directory
  28. var knownEntries = []string{
  29. "css",
  30. "fomantic",
  31. "img",
  32. "js",
  33. "vendor",
  34. }
  35. // Custom implements the macaron static handler for serving custom assets.
  36. func Custom(opts *Options) macaron.Handler {
  37. return opts.staticHandler(path.Join(setting.CustomPath, "public"))
  38. }
  39. // staticFileSystem implements http.FileSystem interface.
  40. type staticFileSystem struct {
  41. dir *http.Dir
  42. }
  43. func newStaticFileSystem(directory string) staticFileSystem {
  44. if !filepath.IsAbs(directory) {
  45. directory = filepath.Join(macaron.Root, directory)
  46. }
  47. dir := http.Dir(directory)
  48. return staticFileSystem{&dir}
  49. }
  50. func (fs staticFileSystem) Open(name string) (http.File, error) {
  51. return fs.dir.Open(name)
  52. }
  53. // StaticHandler sets up a new middleware for serving static files in the
  54. func StaticHandler(dir string, opts *Options) macaron.Handler {
  55. return opts.staticHandler(dir)
  56. }
  57. func (opts *Options) staticHandler(dir string) macaron.Handler {
  58. // Defaults
  59. if len(opts.IndexFile) == 0 {
  60. opts.IndexFile = "index.html"
  61. }
  62. // Normalize the prefix if provided
  63. if opts.Prefix != "" {
  64. // Ensure we have a leading '/'
  65. if opts.Prefix[0] != '/' {
  66. opts.Prefix = "/" + opts.Prefix
  67. }
  68. // Remove any trailing '/'
  69. opts.Prefix = strings.TrimRight(opts.Prefix, "/")
  70. }
  71. if opts.FileSystem == nil {
  72. opts.FileSystem = newStaticFileSystem(dir)
  73. }
  74. return func(ctx *macaron.Context, log *log.Logger) {
  75. opts.handle(ctx, log, opts)
  76. }
  77. }
  78. func (opts *Options) handle(ctx *macaron.Context, log *log.Logger, opt *Options) bool {
  79. if ctx.Req.Method != "GET" && ctx.Req.Method != "HEAD" {
  80. return false
  81. }
  82. file := ctx.Req.URL.Path
  83. // if we have a prefix, filter requests by stripping the prefix
  84. if opt.Prefix != "" {
  85. if !strings.HasPrefix(file, opt.Prefix) {
  86. return false
  87. }
  88. file = file[len(opt.Prefix):]
  89. if file != "" && file[0] != '/' {
  90. return false
  91. }
  92. }
  93. f, err := opt.FileSystem.Open(file)
  94. if err != nil {
  95. // 404 requests to any known entries in `public`
  96. if path.Base(opts.Directory) == "public" {
  97. parts := strings.Split(file, "/")
  98. if len(parts) < 2 {
  99. return false
  100. }
  101. for _, entry := range knownEntries {
  102. if entry == parts[1] {
  103. ctx.Resp.WriteHeader(404)
  104. return true
  105. }
  106. }
  107. }
  108. return false
  109. }
  110. defer f.Close()
  111. fi, err := f.Stat()
  112. if err != nil {
  113. log.Printf("[Static] %q exists, but fails to open: %v", file, err)
  114. return true
  115. }
  116. // Try to serve index file
  117. if fi.IsDir() {
  118. // Redirect if missing trailing slash.
  119. if !strings.HasSuffix(ctx.Req.URL.Path, "/") {
  120. http.Redirect(ctx.Resp, ctx.Req.Request, path.Clean(ctx.Req.URL.Path+"/"), http.StatusFound)
  121. return true
  122. }
  123. f, err = opt.FileSystem.Open(file)
  124. if err != nil {
  125. return false // Discard error.
  126. }
  127. defer f.Close()
  128. fi, err = f.Stat()
  129. if err != nil || fi.IsDir() {
  130. return true
  131. }
  132. }
  133. if !opt.SkipLogging {
  134. log.Println("[Static] Serving " + file)
  135. }
  136. // Add an Expires header to the static content
  137. if opt.ExpiresAfter > 0 {
  138. ctx.Resp.Header().Set("Expires", time.Now().Add(opt.ExpiresAfter).UTC().Format(http.TimeFormat))
  139. tag := GenerateETag(string(fi.Size()), fi.Name(), fi.ModTime().UTC().Format(http.TimeFormat))
  140. ctx.Resp.Header().Set("ETag", tag)
  141. if ctx.Req.Header.Get("If-None-Match") == tag {
  142. ctx.Resp.WriteHeader(304)
  143. return false
  144. }
  145. }
  146. http.ServeContent(ctx.Resp, ctx.Req.Request, file, fi.ModTime(), f)
  147. return true
  148. }
  149. // GenerateETag generates an ETag based on size, filename and file modification time
  150. func GenerateETag(fileSize, fileName, modTime string) string {
  151. etag := fileSize + fileName + modTime
  152. return base64.StdEncoding.EncodeToString([]byte(etag))
  153. }