Source file src/pkg/net/http/fs.go
1
2
3
4
5
6
7 package http
8
9 import (
10 "errors"
11 "fmt"
12 "io"
13 "mime"
14 "mime/multipart"
15 "net/textproto"
16 "os"
17 "path"
18 "path/filepath"
19 "strconv"
20 "strings"
21 "time"
22 )
23
24
25
26
27
28 type Dir string
29
30 func (d Dir) Open(name string) (File, error) {
31 if filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0 ||
32 strings.Contains(name, "\x00") {
33 return nil, errors.New("http: invalid character in file path")
34 }
35 dir := string(d)
36 if dir == "" {
37 dir = "."
38 }
39 f, err := os.Open(filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name))))
40 if err != nil {
41 return nil, err
42 }
43 return f, nil
44 }
45
46
47
48
49 type FileSystem interface {
50 Open(name string) (File, error)
51 }
52
53
54
55 type File interface {
56 Close() error
57 Stat() (os.FileInfo, error)
58 Readdir(count int) ([]os.FileInfo, error)
59 Read([]byte) (int, error)
60 Seek(offset int64, whence int) (int64, error)
61 }
62
63 func dirList(w ResponseWriter, f File) {
64 w.Header().Set("Content-Type", "text/html; charset=utf-8")
65 fmt.Fprintf(w, "<pre>\n")
66 for {
67 dirs, err := f.Readdir(100)
68 if err != nil || len(dirs) == 0 {
69 break
70 }
71 for _, d := range dirs {
72 name := d.Name()
73 if d.IsDir() {
74 name += "/"
75 }
76
77 fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", name, name)
78 }
79 }
80 fmt.Fprintf(w, "</pre>\n")
81 }
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107 func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) {
108 size, err := content.Seek(0, os.SEEK_END)
109 if err != nil {
110 Error(w, "seeker can't seek", StatusInternalServerError)
111 return
112 }
113 _, err = content.Seek(0, os.SEEK_SET)
114 if err != nil {
115 Error(w, "seeker can't seek", StatusInternalServerError)
116 return
117 }
118 serveContent(w, req, name, modtime, size, content)
119 }
120
121
122
123
124 func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, size int64, content io.ReadSeeker) {
125 if checkLastModified(w, r, modtime) {
126 return
127 }
128 rangeReq, done := checkETag(w, r)
129 if done {
130 return
131 }
132
133 code := StatusOK
134
135
136 ctype := w.Header().Get("Content-Type")
137 if ctype == "" {
138 ctype = mime.TypeByExtension(filepath.Ext(name))
139 if ctype == "" {
140
141 var buf [1024]byte
142 n, _ := io.ReadFull(content, buf[:])
143 b := buf[:n]
144 ctype = DetectContentType(b)
145 _, err := content.Seek(0, os.SEEK_SET)
146 if err != nil {
147 Error(w, "seeker can't seek", StatusInternalServerError)
148 return
149 }
150 }
151 w.Header().Set("Content-Type", ctype)
152 }
153
154
155 sendSize := size
156 var sendContent io.Reader = content
157 if size >= 0 {
158 ranges, err := parseRange(rangeReq, size)
159 if err != nil {
160 Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
161 return
162 }
163 if sumRangesSize(ranges) >= size {
164
165
166
167
168 ranges = nil
169 }
170 switch {
171 case len(ranges) == 1:
172
173
174
175
176
177
178
179
180
181
182
183 ra := ranges[0]
184 if _, err := content.Seek(ra.start, os.SEEK_SET); err != nil {
185 Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
186 return
187 }
188 sendSize = ra.length
189 code = StatusPartialContent
190 w.Header().Set("Content-Range", ra.contentRange(size))
191 case len(ranges) > 1:
192 for _, ra := range ranges {
193 if ra.start > size {
194 Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
195 return
196 }
197 }
198 sendSize = rangesMIMESize(ranges, ctype, size)
199 code = StatusPartialContent
200
201 pr, pw := io.Pipe()
202 mw := multipart.NewWriter(pw)
203 w.Header().Set("Content-Type", "multipart/byteranges; boundary="+mw.Boundary())
204 sendContent = pr
205 defer pr.Close()
206 go func() {
207 for _, ra := range ranges {
208 part, err := mw.CreatePart(ra.mimeHeader(ctype, size))
209 if err != nil {
210 pw.CloseWithError(err)
211 return
212 }
213 if _, err := content.Seek(ra.start, os.SEEK_SET); err != nil {
214 pw.CloseWithError(err)
215 return
216 }
217 if _, err := io.CopyN(part, content, ra.length); err != nil {
218 pw.CloseWithError(err)
219 return
220 }
221 }
222 mw.Close()
223 pw.Close()
224 }()
225 }
226
227 w.Header().Set("Accept-Ranges", "bytes")
228 if w.Header().Get("Content-Encoding") == "" {
229 w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10))
230 }
231 }
232
233 w.WriteHeader(code)
234
235 if r.Method != "HEAD" {
236 io.CopyN(w, sendContent, sendSize)
237 }
238 }
239
240
241
242 func checkLastModified(w ResponseWriter, r *Request, modtime time.Time) bool {
243 if modtime.IsZero() {
244 return false
245 }
246
247
248
249 if t, err := time.Parse(TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.Before(t.Add(1*time.Second)) {
250 h := w.Header()
251 delete(h, "Content-Type")
252 delete(h, "Content-Length")
253 w.WriteHeader(StatusNotModified)
254 return true
255 }
256 w.Header().Set("Last-Modified", modtime.UTC().Format(TimeFormat))
257 return false
258 }
259
260
261
262
263
264
265 func checkETag(w ResponseWriter, r *Request) (rangeReq string, done bool) {
266 etag := w.Header().get("Etag")
267 rangeReq = r.Header.get("Range")
268
269
270
271
272
273
274
275 if ir := r.Header.get("If-Range"); ir != "" && ir != etag {
276
277
278
279
280 rangeReq = ""
281 }
282
283 if inm := r.Header.get("If-None-Match"); inm != "" {
284
285 if etag == "" {
286 return rangeReq, false
287 }
288
289
290
291
292
293
294 if r.Method != "GET" && r.Method != "HEAD" {
295 return rangeReq, false
296 }
297
298
299
300
301 if inm == etag || inm == "*" {
302 h := w.Header()
303 delete(h, "Content-Type")
304 delete(h, "Content-Length")
305 w.WriteHeader(StatusNotModified)
306 return "", true
307 }
308 }
309 return rangeReq, false
310 }
311
312
313 func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) {
314 const indexPage = "/index.html"
315
316
317
318
319 if strings.HasSuffix(r.URL.Path, indexPage) {
320 localRedirect(w, r, "./")
321 return
322 }
323
324 f, err := fs.Open(name)
325 if err != nil {
326
327 NotFound(w, r)
328 return
329 }
330 defer f.Close()
331
332 d, err1 := f.Stat()
333 if err1 != nil {
334
335 NotFound(w, r)
336 return
337 }
338
339 if redirect {
340
341
342 url := r.URL.Path
343 if d.IsDir() {
344 if url[len(url)-1] != '/' {
345 localRedirect(w, r, path.Base(url)+"/")
346 return
347 }
348 } else {
349 if url[len(url)-1] == '/' {
350 localRedirect(w, r, "../"+path.Base(url))
351 return
352 }
353 }
354 }
355
356
357 if d.IsDir() {
358 index := name + indexPage
359 ff, err := fs.Open(index)
360 if err == nil {
361 defer ff.Close()
362 dd, err := ff.Stat()
363 if err == nil {
364 name = index
365 d = dd
366 f = ff
367 }
368 }
369 }
370
371
372 if d.IsDir() {
373 if checkLastModified(w, r, d.ModTime()) {
374 return
375 }
376 dirList(w, f)
377 return
378 }
379
380
381 serveContent(w, r, d.Name(), d.ModTime(), d.Size(), f)
382 }
383
384
385
386 func localRedirect(w ResponseWriter, r *Request, newPath string) {
387 if q := r.URL.RawQuery; q != "" {
388 newPath += "?" + q
389 }
390 w.Header().Set("Location", newPath)
391 w.WriteHeader(StatusMovedPermanently)
392 }
393
394
395 func ServeFile(w ResponseWriter, r *Request, name string) {
396 dir, file := filepath.Split(name)
397 serveFile(w, r, Dir(dir), file, false)
398 }
399
400 type fileHandler struct {
401 root FileSystem
402 }
403
404
405
406
407
408
409
410
411 func FileServer(root FileSystem) Handler {
412 return &fileHandler{root}
413 }
414
415 func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) {
416 upath := r.URL.Path
417 if !strings.HasPrefix(upath, "/") {
418 upath = "/" + upath
419 r.URL.Path = upath
420 }
421 serveFile(w, r, f.root, path.Clean(upath), true)
422 }
423
424
425 type httpRange struct {
426 start, length int64
427 }
428
429 func (r httpRange) contentRange(size int64) string {
430 return fmt.Sprintf("bytes %d-%d/%d", r.start, r.start+r.length-1, size)
431 }
432
433 func (r httpRange) mimeHeader(contentType string, size int64) textproto.MIMEHeader {
434 return textproto.MIMEHeader{
435 "Content-Range": {r.contentRange(size)},
436 "Content-Type": {contentType},
437 }
438 }
439
440
441 func parseRange(s string, size int64) ([]httpRange, error) {
442 if s == "" {
443 return nil, nil
444 }
445 const b = "bytes="
446 if !strings.HasPrefix(s, b) {
447 return nil, errors.New("invalid range")
448 }
449 var ranges []httpRange
450 for _, ra := range strings.Split(s[len(b):], ",") {
451 ra = strings.TrimSpace(ra)
452 if ra == "" {
453 continue
454 }
455 i := strings.Index(ra, "-")
456 if i < 0 {
457 return nil, errors.New("invalid range")
458 }
459 start, end := strings.TrimSpace(ra[:i]), strings.TrimSpace(ra[i+1:])
460 var r httpRange
461 if start == "" {
462
463
464 i, err := strconv.ParseInt(end, 10, 64)
465 if err != nil {
466 return nil, errors.New("invalid range")
467 }
468 if i > size {
469 i = size
470 }
471 r.start = size - i
472 r.length = size - r.start
473 } else {
474 i, err := strconv.ParseInt(start, 10, 64)
475 if err != nil || i > size || i < 0 {
476 return nil, errors.New("invalid range")
477 }
478 r.start = i
479 if end == "" {
480
481 r.length = size - r.start
482 } else {
483 i, err := strconv.ParseInt(end, 10, 64)
484 if err != nil || r.start > i {
485 return nil, errors.New("invalid range")
486 }
487 if i >= size {
488 i = size - 1
489 }
490 r.length = i - r.start + 1
491 }
492 }
493 ranges = append(ranges, r)
494 }
495 return ranges, nil
496 }
497
498
499 type countingWriter int64
500
501 func (w *countingWriter) Write(p []byte) (n int, err error) {
502 *w += countingWriter(len(p))
503 return len(p), nil
504 }
505
506
507
508 func rangesMIMESize(ranges []httpRange, contentType string, contentSize int64) (encSize int64) {
509 var w countingWriter
510 mw := multipart.NewWriter(&w)
511 for _, ra := range ranges {
512 mw.CreatePart(ra.mimeHeader(contentType, contentSize))
513 encSize += ra.length
514 }
515 mw.Close()
516 encSize += int64(w)
517 return
518 }
519
520 func sumRangesSize(ranges []httpRange) (size int64) {
521 for _, ra := range ranges {
522 size += ra.length
523 }
524 return
525 }
View as plain text