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.
 
 
 

266 lines
6.1 KiB

  1. // Copyright 2013 The Go Authors. All rights reserved.
  2. //
  3. // Use of this source code is governed by a BSD-style
  4. // license that can be found in the LICENSE file or at
  5. // https://developers.google.com/open-source/licenses/bsd.
  6. package httputil
  7. import (
  8. "bytes"
  9. "crypto/sha1"
  10. "errors"
  11. "fmt"
  12. "github.com/golang/gddo/httputil/header"
  13. "io"
  14. "io/ioutil"
  15. "mime"
  16. "net/http"
  17. "os"
  18. "path"
  19. "path/filepath"
  20. "strconv"
  21. "strings"
  22. "sync"
  23. "time"
  24. )
  25. // StaticServer serves static files.
  26. type StaticServer struct {
  27. // Dir specifies the location of the directory containing the files to serve.
  28. Dir string
  29. // MaxAge specifies the maximum age for the cache control and expiration
  30. // headers.
  31. MaxAge time.Duration
  32. // Error specifies the function used to generate error responses. If Error
  33. // is nil, then http.Error is used to generate error responses.
  34. Error Error
  35. // MIMETypes is a map from file extensions to MIME types.
  36. MIMETypes map[string]string
  37. mu sync.Mutex
  38. etags map[string]string
  39. }
  40. func (ss *StaticServer) resolve(fname string) string {
  41. if path.IsAbs(fname) {
  42. panic("Absolute path not allowed when creating a StaticServer handler")
  43. }
  44. dir := ss.Dir
  45. if dir == "" {
  46. dir = "."
  47. }
  48. fname = filepath.FromSlash(fname)
  49. return filepath.Join(dir, fname)
  50. }
  51. func (ss *StaticServer) mimeType(fname string) string {
  52. ext := path.Ext(fname)
  53. var mimeType string
  54. if ss.MIMETypes != nil {
  55. mimeType = ss.MIMETypes[ext]
  56. }
  57. if mimeType == "" {
  58. mimeType = mime.TypeByExtension(ext)
  59. }
  60. if mimeType == "" {
  61. mimeType = "application/octet-stream"
  62. }
  63. return mimeType
  64. }
  65. func (ss *StaticServer) openFile(fname string) (io.ReadCloser, int64, string, error) {
  66. f, err := os.Open(fname)
  67. if err != nil {
  68. return nil, 0, "", err
  69. }
  70. fi, err := f.Stat()
  71. if err != nil {
  72. f.Close()
  73. return nil, 0, "", err
  74. }
  75. const modeType = os.ModeDir | os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice
  76. if fi.Mode()&modeType != 0 {
  77. f.Close()
  78. return nil, 0, "", errors.New("not a regular file")
  79. }
  80. return f, fi.Size(), ss.mimeType(fname), nil
  81. }
  82. // FileHandler returns a handler that serves a single file. The file is
  83. // specified by a slash separated path relative to the static server's Dir
  84. // field.
  85. func (ss *StaticServer) FileHandler(fileName string) http.Handler {
  86. id := fileName
  87. fileName = ss.resolve(fileName)
  88. return &staticHandler{
  89. ss: ss,
  90. id: func(_ string) string { return id },
  91. open: func(_ string) (io.ReadCloser, int64, string, error) { return ss.openFile(fileName) },
  92. }
  93. }
  94. // DirectoryHandler returns a handler that serves files from a directory tree.
  95. // The directory is specified by a slash separated path relative to the static
  96. // server's Dir field.
  97. func (ss *StaticServer) DirectoryHandler(prefix, dirName string) http.Handler {
  98. if !strings.HasSuffix(prefix, "/") {
  99. prefix += "/"
  100. }
  101. idBase := dirName
  102. dirName = ss.resolve(dirName)
  103. return &staticHandler{
  104. ss: ss,
  105. id: func(p string) string {
  106. if !strings.HasPrefix(p, prefix) {
  107. return "."
  108. }
  109. return path.Join(idBase, p[len(prefix):])
  110. },
  111. open: func(p string) (io.ReadCloser, int64, string, error) {
  112. if !strings.HasPrefix(p, prefix) {
  113. return nil, 0, "", errors.New("request url does not match directory prefix")
  114. }
  115. p = p[len(prefix):]
  116. return ss.openFile(filepath.Join(dirName, filepath.FromSlash(p)))
  117. },
  118. }
  119. }
  120. // FilesHandler returns a handler that serves the concatentation of the
  121. // specified files. The files are specified by slash separated paths relative
  122. // to the static server's Dir field.
  123. func (ss *StaticServer) FilesHandler(fileNames ...string) http.Handler {
  124. // todo: cache concatenated files on disk and serve from there.
  125. mimeType := ss.mimeType(fileNames[0])
  126. var buf []byte
  127. var openErr error
  128. for _, fileName := range fileNames {
  129. p, err := ioutil.ReadFile(ss.resolve(fileName))
  130. if err != nil {
  131. openErr = err
  132. buf = nil
  133. break
  134. }
  135. buf = append(buf, p...)
  136. }
  137. id := strings.Join(fileNames, " ")
  138. return &staticHandler{
  139. ss: ss,
  140. id: func(_ string) string { return id },
  141. open: func(p string) (io.ReadCloser, int64, string, error) {
  142. return ioutil.NopCloser(bytes.NewReader(buf)), int64(len(buf)), mimeType, openErr
  143. },
  144. }
  145. }
  146. type staticHandler struct {
  147. id func(fname string) string
  148. open func(p string) (io.ReadCloser, int64, string, error)
  149. ss *StaticServer
  150. }
  151. func (h *staticHandler) error(w http.ResponseWriter, r *http.Request, status int, err error) {
  152. http.Error(w, http.StatusText(status), status)
  153. }
  154. func (h *staticHandler) etag(p string) (string, error) {
  155. id := h.id(p)
  156. h.ss.mu.Lock()
  157. if h.ss.etags == nil {
  158. h.ss.etags = make(map[string]string)
  159. }
  160. etag := h.ss.etags[id]
  161. h.ss.mu.Unlock()
  162. if etag != "" {
  163. return etag, nil
  164. }
  165. // todo: if a concurrent goroutine is calculating the hash, then wait for
  166. // it instead of computing it again here.
  167. rc, _, _, err := h.open(p)
  168. if err != nil {
  169. return "", err
  170. }
  171. defer rc.Close()
  172. w := sha1.New()
  173. _, err = io.Copy(w, rc)
  174. if err != nil {
  175. return "", err
  176. }
  177. etag = fmt.Sprintf(`"%x"`, w.Sum(nil))
  178. h.ss.mu.Lock()
  179. h.ss.etags[id] = etag
  180. h.ss.mu.Unlock()
  181. return etag, nil
  182. }
  183. func (h *staticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  184. p := path.Clean(r.URL.Path)
  185. if p != r.URL.Path {
  186. http.Redirect(w, r, p, 301)
  187. return
  188. }
  189. etag, err := h.etag(p)
  190. if err != nil {
  191. h.error(w, r, http.StatusNotFound, err)
  192. return
  193. }
  194. maxAge := h.ss.MaxAge
  195. if maxAge == 0 {
  196. maxAge = 24 * time.Hour
  197. }
  198. if r.FormValue("v") != "" {
  199. maxAge = 365 * 24 * time.Hour
  200. }
  201. cacheControl := fmt.Sprintf("public, max-age=%d", maxAge/time.Second)
  202. for _, e := range header.ParseList(r.Header, "If-None-Match") {
  203. if e == etag {
  204. w.Header().Set("Cache-Control", cacheControl)
  205. w.Header().Set("Etag", etag)
  206. w.WriteHeader(http.StatusNotModified)
  207. return
  208. }
  209. }
  210. rc, cl, ct, err := h.open(p)
  211. if err != nil {
  212. h.error(w, r, http.StatusNotFound, err)
  213. return
  214. }
  215. defer rc.Close()
  216. w.Header().Set("Cache-Control", cacheControl)
  217. w.Header().Set("Etag", etag)
  218. if ct != "" {
  219. w.Header().Set("Content-Type", ct)
  220. }
  221. if cl != 0 {
  222. w.Header().Set("Content-Length", strconv.FormatInt(cl, 10))
  223. }
  224. w.WriteHeader(http.StatusOK)
  225. if r.Method != "HEAD" {
  226. io.Copy(w, rc)
  227. }
  228. }