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.
 
 
 

233 lines
6.3 KiB

  1. // Copyright 2015 Google Inc. All rights reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. // Package static provides a modifier that allows Martian to return static files
  15. // local to Martian. The static modifier does not support setting explicit path
  16. // mappings via the JSON API.
  17. package static
  18. import (
  19. "bytes"
  20. "encoding/json"
  21. "fmt"
  22. "io"
  23. "io/ioutil"
  24. "mime"
  25. "mime/multipart"
  26. "net/http"
  27. "net/textproto"
  28. "os"
  29. "path"
  30. "path/filepath"
  31. "strconv"
  32. "strings"
  33. "github.com/google/martian"
  34. "github.com/google/martian/parse"
  35. )
  36. // Modifier is a martian.RequestResponseModifier that routes reqeusts to rootPath
  37. // and serves the assets there, while skipping the HTTP roundtrip.
  38. type Modifier struct {
  39. rootPath string
  40. explicitPaths map[string]string
  41. }
  42. type staticJSON struct {
  43. ExplicitPaths map[string]string `json:"explicitPaths"`
  44. RootPath string `json:"rootPath"`
  45. Scope []parse.ModifierType `json:"scope"`
  46. }
  47. func init() {
  48. parse.Register("static.Modifier", modifierFromJSON)
  49. }
  50. // NewModifier constructs a static.Modifier that takes a path to serve files from, as well as an optional mapping of request paths to local
  51. // file paths (still rooted at rootPath).
  52. func NewModifier(rootPath string) *Modifier {
  53. return &Modifier{
  54. rootPath: path.Clean(rootPath),
  55. explicitPaths: make(map[string]string),
  56. }
  57. }
  58. // ModifyRequest marks the context to skip the roundtrip and downgrades any https requests
  59. // to http.
  60. func (s *Modifier) ModifyRequest(req *http.Request) error {
  61. ctx := martian.NewContext(req)
  62. ctx.SkipRoundTrip()
  63. return nil
  64. }
  65. // ModifyResponse reads the file rooted at rootPath joined with the request URL
  66. // path. In the case that the the request path is a key in s.explicitPaths, ModifyRequest
  67. // will attempt to open the file located at s.rootPath joined by the value in s.explicitPaths
  68. // (keyed by res.Request.URL.Path). In the case that the file cannot be found, the response
  69. // will be a 404. ModifyResponse will return a 404 for any path that is defined in s.explictPaths
  70. // and that does not exist locally, even if that file does exist in s.rootPath.
  71. func (s *Modifier) ModifyResponse(res *http.Response) error {
  72. reqpth := filepath.Clean(res.Request.URL.Path)
  73. fpth := filepath.Join(s.rootPath, reqpth)
  74. if _, ok := s.explicitPaths[reqpth]; ok {
  75. fpth = filepath.Join(s.rootPath, s.explicitPaths[reqpth])
  76. }
  77. f, err := os.Open(fpth)
  78. switch {
  79. case os.IsNotExist(err):
  80. res.StatusCode = http.StatusNotFound
  81. return nil
  82. case os.IsPermission(err):
  83. // This is returning a StatusUnauthorized to reflect that the Martian does
  84. // not have the appropriate permissions on the local file system. This is a
  85. // deviation from the standard assumption around an HTTP 401 response.
  86. res.StatusCode = http.StatusUnauthorized
  87. return err
  88. case err != nil:
  89. res.StatusCode = http.StatusInternalServerError
  90. return err
  91. }
  92. res.Body.Close()
  93. info, err := f.Stat()
  94. if err != nil {
  95. res.StatusCode = http.StatusInternalServerError
  96. return err
  97. }
  98. contentType := mime.TypeByExtension(filepath.Ext(fpth))
  99. res.Header.Set("Content-Type", contentType)
  100. // If no range request header is present, return the file as the response body.
  101. if res.Request.Header.Get("Range") == "" {
  102. res.ContentLength = info.Size()
  103. res.Body = f
  104. return nil
  105. }
  106. rh := res.Request.Header.Get("Range")
  107. rh = strings.ToLower(rh)
  108. sranges := strings.Split(strings.TrimLeft(rh, "bytes="), ",")
  109. var ranges [][]int
  110. for _, rng := range sranges {
  111. if strings.HasSuffix(rng, "-") {
  112. rng = fmt.Sprintf("%s%d", rng, info.Size()-1)
  113. }
  114. rs := strings.Split(rng, "-")
  115. if len(rs) != 2 {
  116. res.StatusCode = http.StatusRequestedRangeNotSatisfiable
  117. return nil
  118. }
  119. start, err := strconv.Atoi(strings.TrimSpace(rs[0]))
  120. if err != nil {
  121. return err
  122. }
  123. end, err := strconv.Atoi(strings.TrimSpace(rs[1]))
  124. if err != nil {
  125. return err
  126. }
  127. if start > end {
  128. res.StatusCode = http.StatusRequestedRangeNotSatisfiable
  129. return nil
  130. }
  131. ranges = append(ranges, []int{start, end})
  132. }
  133. // Range request.
  134. res.StatusCode = http.StatusPartialContent
  135. // Single range request.
  136. if len(ranges) == 1 {
  137. start := ranges[0][0]
  138. end := ranges[0][1]
  139. length := end - start + 1
  140. seg := make([]byte, length)
  141. switch n, err := f.ReadAt(seg, int64(start)); err {
  142. case nil, io.EOF:
  143. res.ContentLength = int64(n)
  144. default:
  145. return err
  146. }
  147. res.Body = ioutil.NopCloser(bytes.NewReader(seg))
  148. res.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, info.Size()))
  149. return nil
  150. }
  151. // Multipart range request.
  152. var mpbody bytes.Buffer
  153. mpw := multipart.NewWriter(&mpbody)
  154. for _, rng := range ranges {
  155. start, end := rng[0], rng[1]
  156. mimeh := make(textproto.MIMEHeader)
  157. mimeh.Set("Content-Type", contentType)
  158. mimeh.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, info.Size()))
  159. length := end - start + 1
  160. seg := make([]byte, length)
  161. switch n, err := f.ReadAt(seg, int64(start)); err {
  162. case nil, io.EOF:
  163. res.ContentLength = int64(n)
  164. default:
  165. return err
  166. }
  167. pw, err := mpw.CreatePart(mimeh)
  168. if err != nil {
  169. return err
  170. }
  171. if _, err := pw.Write(seg); err != nil {
  172. return err
  173. }
  174. }
  175. mpw.Close()
  176. res.ContentLength = int64(len(mpbody.Bytes()))
  177. res.Body = ioutil.NopCloser(bytes.NewReader(mpbody.Bytes()))
  178. res.Header.Set("Content-Type", fmt.Sprintf("multipart/byteranges; boundary=%s", mpw.Boundary()))
  179. return nil
  180. }
  181. // SetExplicitPathMappings sets an optional mapping of request paths to local
  182. // file paths rooted at s.rootPath.
  183. func (s *Modifier) SetExplicitPathMappings(ep map[string]string) {
  184. s.explicitPaths = ep
  185. }
  186. func modifierFromJSON(b []byte) (*parse.Result, error) {
  187. msg := &staticJSON{}
  188. if err := json.Unmarshal(b, msg); err != nil {
  189. return nil, err
  190. }
  191. mod := NewModifier(msg.RootPath)
  192. mod.SetExplicitPathMappings(msg.ExplicitPaths)
  193. return parse.NewResult(mod, msg.Scope)
  194. }