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

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