1
2
3
4
5
6
7 package http
8
9 import (
10 "errors"
11 "fmt"
12 "io"
13 "io/fs"
14 "mime"
15 "mime/multipart"
16 "net/textproto"
17 "net/url"
18 "os"
19 "path"
20 "path/filepath"
21 "sort"
22 "strconv"
23 "strings"
24 "time"
25 )
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 type Dir string
44
45
46
47
48 func mapDirOpenError(originalErr error, name string) error {
49 if errors.Is(originalErr, fs.ErrNotExist) || errors.Is(originalErr, fs.ErrPermission) {
50 return originalErr
51 }
52
53 parts := strings.Split(name, string(filepath.Separator))
54 for i := range parts {
55 if parts[i] == "" {
56 continue
57 }
58 fi, err := os.Stat(strings.Join(parts[:i+1], string(filepath.Separator)))
59 if err != nil {
60 return originalErr
61 }
62 if !fi.IsDir() {
63 return fs.ErrNotExist
64 }
65 }
66 return originalErr
67 }
68
69
70
71 func (d Dir) Open(name string) (File, error) {
72 if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) {
73 return nil, errors.New("http: invalid character in file path")
74 }
75 dir := string(d)
76 if dir == "" {
77 dir = "."
78 }
79 fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name)))
80 f, err := os.Open(fullName)
81 if err != nil {
82 return nil, mapDirOpenError(err, fullName)
83 }
84 return f, nil
85 }
86
87
88
89
90
91
92
93
94 type FileSystem interface {
95 Open(name string) (File, error)
96 }
97
98
99
100
101
102 type File interface {
103 io.Closer
104 io.Reader
105 io.Seeker
106 Readdir(count int) ([]fs.FileInfo, error)
107 Stat() (fs.FileInfo, error)
108 }
109
110 type anyDirs interface {
111 len() int
112 name(i int) string
113 isDir(i int) bool
114 }
115
116 type fileInfoDirs []fs.FileInfo
117
118 func (d fileInfoDirs) len() int { return len(d) }
119 func (d fileInfoDirs) isDir(i int) bool { return d[i].IsDir() }
120 func (d fileInfoDirs) name(i int) string { return d[i].Name() }
121
122 type dirEntryDirs []fs.DirEntry
123
124 func (d dirEntryDirs) len() int { return len(d) }
125 func (d dirEntryDirs) isDir(i int) bool { return d[i].IsDir() }
126 func (d dirEntryDirs) name(i int) string { return d[i].Name() }
127
128 func dirList(w ResponseWriter, r *Request, f File) {
129
130
131
132 var dirs anyDirs
133 var err error
134 if d, ok := f.(fs.ReadDirFile); ok {
135 var list dirEntryDirs
136 list, err = d.ReadDir(-1)
137 dirs = list
138 } else {
139 var list fileInfoDirs
140 list, err = f.Readdir(-1)
141 dirs = list
142 }
143
144 if err != nil {
145 logf(r, "http: error reading directory: %v", err)
146 Error(w, "Error reading directory", StatusInternalServerError)
147 return
148 }
149 sort.Slice(dirs, func(i, j int) bool { return dirs.name(i) < dirs.name(j) })
150
151 w.Header().Set("Content-Type", "text/html; charset=utf-8")
152 fmt.Fprintf(w, "<pre>\n")
153 for i, n := 0, dirs.len(); i < n; i++ {
154 name := dirs.name(i)
155 if dirs.isDir(i) {
156 name += "/"
157 }
158
159
160
161 url := url.URL{Path: name}
162 fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", url.String(), htmlReplacer.Replace(name))
163 }
164 fmt.Fprintf(w, "</pre>\n")
165 }
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192 func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) {
193 sizeFunc := func() (int64, error) {
194 size, err := content.Seek(0, io.SeekEnd)
195 if err != nil {
196 return 0, errSeeker
197 }
198 _, err = content.Seek(0, io.SeekStart)
199 if err != nil {
200 return 0, errSeeker
201 }
202 return size, nil
203 }
204 serveContent(w, req, name, modtime, sizeFunc, content)
205 }
206
207
208
209
210
211 var errSeeker = errors.New("seeker can't seek")
212
213
214
215 var errNoOverlap = errors.New("invalid range: failed to overlap")
216
217
218
219
220
221 func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) {
222 setLastModified(w, modtime)
223 done, rangeReq := checkPreconditions(w, r, modtime)
224 if done {
225 return
226 }
227
228 code := StatusOK
229
230
231
232 ctypes, haveType := w.Header()["Content-Type"]
233 var ctype string
234 if !haveType {
235 ctype = mime.TypeByExtension(filepath.Ext(name))
236 if ctype == "" {
237
238 var buf [sniffLen]byte
239 n, _ := io.ReadFull(content, buf[:])
240 ctype = DetectContentType(buf[:n])
241 _, err := content.Seek(0, io.SeekStart)
242 if err != nil {
243 Error(w, "seeker can't seek", StatusInternalServerError)
244 return
245 }
246 }
247 w.Header().Set("Content-Type", ctype)
248 } else if len(ctypes) > 0 {
249 ctype = ctypes[0]
250 }
251
252 size, err := sizeFunc()
253 if err != nil {
254 Error(w, err.Error(), StatusInternalServerError)
255 return
256 }
257
258
259 sendSize := size
260 var sendContent io.Reader = content
261 if size >= 0 {
262 ranges, err := parseRange(rangeReq, size)
263 if err != nil {
264 if err == errNoOverlap {
265 w.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", size))
266 }
267 Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
268 return
269 }
270 if sumRangesSize(ranges) > size {
271
272
273
274
275 ranges = nil
276 }
277 switch {
278 case len(ranges) == 1:
279
280
281
282
283
284
285
286
287
288
289
290 ra := ranges[0]
291 if _, err := content.Seek(ra.start, io.SeekStart); err != nil {
292 Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
293 return
294 }
295 sendSize = ra.length
296 code = StatusPartialContent
297 w.Header().Set("Content-Range", ra.contentRange(size))
298 case len(ranges) > 1:
299 sendSize = rangesMIMESize(ranges, ctype, size)
300 code = StatusPartialContent
301
302 pr, pw := io.Pipe()
303 mw := multipart.NewWriter(pw)
304 w.Header().Set("Content-Type", "multipart/byteranges; boundary="+mw.Boundary())
305 sendContent = pr
306 defer pr.Close()
307 go func() {
308 for _, ra := range ranges {
309 part, err := mw.CreatePart(ra.mimeHeader(ctype, size))
310 if err != nil {
311 pw.CloseWithError(err)
312 return
313 }
314 if _, err := content.Seek(ra.start, io.SeekStart); err != nil {
315 pw.CloseWithError(err)
316 return
317 }
318 if _, err := io.CopyN(part, content, ra.length); err != nil {
319 pw.CloseWithError(err)
320 return
321 }
322 }
323 mw.Close()
324 pw.Close()
325 }()
326 }
327
328 w.Header().Set("Accept-Ranges", "bytes")
329 if w.Header().Get("Content-Encoding") == "" {
330 w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10))
331 }
332 }
333
334 w.WriteHeader(code)
335
336 if r.Method != "HEAD" {
337 io.CopyN(w, sendContent, sendSize)
338 }
339 }
340
341
342
343
344 func scanETag(s string) (etag string, remain string) {
345 s = textproto.TrimString(s)
346 start := 0
347 if strings.HasPrefix(s, "W/") {
348 start = 2
349 }
350 if len(s[start:]) < 2 || s[start] != '"' {
351 return "", ""
352 }
353
354
355 for i := start + 1; i < len(s); i++ {
356 c := s[i]
357 switch {
358
359 case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80:
360 case c == '"':
361 return s[:i+1], s[i+1:]
362 default:
363 return "", ""
364 }
365 }
366 return "", ""
367 }
368
369
370
371 func etagStrongMatch(a, b string) bool {
372 return a == b && a != "" && a[0] == '"'
373 }
374
375
376
377 func etagWeakMatch(a, b string) bool {
378 return strings.TrimPrefix(a, "W/") == strings.TrimPrefix(b, "W/")
379 }
380
381
382
383 type condResult int
384
385 const (
386 condNone condResult = iota
387 condTrue
388 condFalse
389 )
390
391 func checkIfMatch(w ResponseWriter, r *Request) condResult {
392 im := r.Header.Get("If-Match")
393 if im == "" {
394 return condNone
395 }
396 for {
397 im = textproto.TrimString(im)
398 if len(im) == 0 {
399 break
400 }
401 if im[0] == ',' {
402 im = im[1:]
403 continue
404 }
405 if im[0] == '*' {
406 return condTrue
407 }
408 etag, remain := scanETag(im)
409 if etag == "" {
410 break
411 }
412 if etagStrongMatch(etag, w.Header().get("Etag")) {
413 return condTrue
414 }
415 im = remain
416 }
417
418 return condFalse
419 }
420
421 func checkIfUnmodifiedSince(r *Request, modtime time.Time) condResult {
422 ius := r.Header.Get("If-Unmodified-Since")
423 if ius == "" || isZeroTime(modtime) {
424 return condNone
425 }
426 t, err := ParseTime(ius)
427 if err != nil {
428 return condNone
429 }
430
431
432
433 modtime = modtime.Truncate(time.Second)
434 if modtime.Before(t) || modtime.Equal(t) {
435 return condTrue
436 }
437 return condFalse
438 }
439
440 func checkIfNoneMatch(w ResponseWriter, r *Request) condResult {
441 inm := r.Header.get("If-None-Match")
442 if inm == "" {
443 return condNone
444 }
445 buf := inm
446 for {
447 buf = textproto.TrimString(buf)
448 if len(buf) == 0 {
449 break
450 }
451 if buf[0] == ',' {
452 buf = buf[1:]
453 continue
454 }
455 if buf[0] == '*' {
456 return condFalse
457 }
458 etag, remain := scanETag(buf)
459 if etag == "" {
460 break
461 }
462 if etagWeakMatch(etag, w.Header().get("Etag")) {
463 return condFalse
464 }
465 buf = remain
466 }
467 return condTrue
468 }
469
470 func checkIfModifiedSince(r *Request, modtime time.Time) condResult {
471 if r.Method != "GET" && r.Method != "HEAD" {
472 return condNone
473 }
474 ims := r.Header.Get("If-Modified-Since")
475 if ims == "" || isZeroTime(modtime) {
476 return condNone
477 }
478 t, err := ParseTime(ims)
479 if err != nil {
480 return condNone
481 }
482
483
484 modtime = modtime.Truncate(time.Second)
485 if modtime.Before(t) || modtime.Equal(t) {
486 return condFalse
487 }
488 return condTrue
489 }
490
491 func checkIfRange(w ResponseWriter, r *Request, modtime time.Time) condResult {
492 if r.Method != "GET" && r.Method != "HEAD" {
493 return condNone
494 }
495 ir := r.Header.get("If-Range")
496 if ir == "" {
497 return condNone
498 }
499 etag, _ := scanETag(ir)
500 if etag != "" {
501 if etagStrongMatch(etag, w.Header().Get("Etag")) {
502 return condTrue
503 } else {
504 return condFalse
505 }
506 }
507
508
509 if modtime.IsZero() {
510 return condFalse
511 }
512 t, err := ParseTime(ir)
513 if err != nil {
514 return condFalse
515 }
516 if t.Unix() == modtime.Unix() {
517 return condTrue
518 }
519 return condFalse
520 }
521
522 var unixEpochTime = time.Unix(0, 0)
523
524
525 func isZeroTime(t time.Time) bool {
526 return t.IsZero() || t.Equal(unixEpochTime)
527 }
528
529 func setLastModified(w ResponseWriter, modtime time.Time) {
530 if !isZeroTime(modtime) {
531 w.Header().Set("Last-Modified", modtime.UTC().Format(TimeFormat))
532 }
533 }
534
535 func writeNotModified(w ResponseWriter) {
536
537
538
539
540
541 h := w.Header()
542 delete(h, "Content-Type")
543 delete(h, "Content-Length")
544 if h.Get("Etag") != "" {
545 delete(h, "Last-Modified")
546 }
547 w.WriteHeader(StatusNotModified)
548 }
549
550
551
552 func checkPreconditions(w ResponseWriter, r *Request, modtime time.Time) (done bool, rangeHeader string) {
553
554 ch := checkIfMatch(w, r)
555 if ch == condNone {
556 ch = checkIfUnmodifiedSince(r, modtime)
557 }
558 if ch == condFalse {
559 w.WriteHeader(StatusPreconditionFailed)
560 return true, ""
561 }
562 switch checkIfNoneMatch(w, r) {
563 case condFalse:
564 if r.Method == "GET" || r.Method == "HEAD" {
565 writeNotModified(w)
566 return true, ""
567 } else {
568 w.WriteHeader(StatusPreconditionFailed)
569 return true, ""
570 }
571 case condNone:
572 if checkIfModifiedSince(r, modtime) == condFalse {
573 writeNotModified(w)
574 return true, ""
575 }
576 }
577
578 rangeHeader = r.Header.get("Range")
579 if rangeHeader != "" && checkIfRange(w, r, modtime) == condFalse {
580 rangeHeader = ""
581 }
582 return false, rangeHeader
583 }
584
585
586 func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) {
587 const indexPage = "/index.html"
588
589
590
591
592 if strings.HasSuffix(r.URL.Path, indexPage) {
593 localRedirect(w, r, "./")
594 return
595 }
596
597 f, err := fs.Open(name)
598 if err != nil {
599 msg, code := toHTTPError(err)
600 Error(w, msg, code)
601 return
602 }
603 defer f.Close()
604
605 d, err := f.Stat()
606 if err != nil {
607 msg, code := toHTTPError(err)
608 Error(w, msg, code)
609 return
610 }
611
612 if redirect {
613
614
615 url := r.URL.Path
616 if d.IsDir() {
617 if url[len(url)-1] != '/' {
618 localRedirect(w, r, path.Base(url)+"/")
619 return
620 }
621 } else {
622 if url[len(url)-1] == '/' {
623 localRedirect(w, r, "../"+path.Base(url))
624 return
625 }
626 }
627 }
628
629 if d.IsDir() {
630 url := r.URL.Path
631
632 if url == "" || url[len(url)-1] != '/' {
633 localRedirect(w, r, path.Base(url)+"/")
634 return
635 }
636
637
638 index := strings.TrimSuffix(name, "/") + indexPage
639 ff, err := fs.Open(index)
640 if err == nil {
641 defer ff.Close()
642 dd, err := ff.Stat()
643 if err == nil {
644 name = index
645 d = dd
646 f = ff
647 }
648 }
649 }
650
651
652 if d.IsDir() {
653 if checkIfModifiedSince(r, d.ModTime()) == condFalse {
654 writeNotModified(w)
655 return
656 }
657 setLastModified(w, d.ModTime())
658 dirList(w, r, f)
659 return
660 }
661
662
663 sizeFunc := func() (int64, error) { return d.Size(), nil }
664 serveContent(w, r, d.Name(), d.ModTime(), sizeFunc, f)
665 }
666
667
668
669
670
671
672 func toHTTPError(err error) (msg string, httpStatus int) {
673 if errors.Is(err, fs.ErrNotExist) {
674 return "404 page not found", StatusNotFound
675 }
676 if errors.Is(err, fs.ErrPermission) {
677 return "403 Forbidden", StatusForbidden
678 }
679
680 return "500 Internal Server Error", StatusInternalServerError
681 }
682
683
684
685 func localRedirect(w ResponseWriter, r *Request, newPath string) {
686 if q := r.URL.RawQuery; q != "" {
687 newPath += "?" + q
688 }
689 w.Header().Set("Location", newPath)
690 w.WriteHeader(StatusMovedPermanently)
691 }
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714 func ServeFile(w ResponseWriter, r *Request, name string) {
715 if containsDotDot(r.URL.Path) {
716
717
718
719
720
721 Error(w, "invalid URL path", StatusBadRequest)
722 return
723 }
724 dir, file := filepath.Split(name)
725 serveFile(w, r, Dir(dir), file, false)
726 }
727
728 func containsDotDot(v string) bool {
729 if !strings.Contains(v, "..") {
730 return false
731 }
732 for _, ent := range strings.FieldsFunc(v, isSlashRune) {
733 if ent == ".." {
734 return true
735 }
736 }
737 return false
738 }
739
740 func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
741
742 type fileHandler struct {
743 root FileSystem
744 }
745
746 type ioFS struct {
747 fsys fs.FS
748 }
749
750 type ioFile struct {
751 file fs.File
752 }
753
754 func (f ioFS) Open(name string) (File, error) {
755 if name == "/" {
756 name = "."
757 } else {
758 name = strings.TrimPrefix(name, "/")
759 }
760 file, err := f.fsys.Open(name)
761 if err != nil {
762 return nil, err
763 }
764 return ioFile{file}, nil
765 }
766
767 func (f ioFile) Close() error { return f.file.Close() }
768 func (f ioFile) Read(b []byte) (int, error) { return f.file.Read(b) }
769 func (f ioFile) Stat() (fs.FileInfo, error) { return f.file.Stat() }
770
771 var errMissingSeek = errors.New("io.File missing Seek method")
772 var errMissingReadDir = errors.New("io.File directory missing ReadDir method")
773
774 func (f ioFile) Seek(offset int64, whence int) (int64, error) {
775 s, ok := f.file.(io.Seeker)
776 if !ok {
777 return 0, errMissingSeek
778 }
779 return s.Seek(offset, whence)
780 }
781
782 func (f ioFile) ReadDir(count int) ([]fs.DirEntry, error) {
783 d, ok := f.file.(fs.ReadDirFile)
784 if !ok {
785 return nil, errMissingReadDir
786 }
787 return d.ReadDir(count)
788 }
789
790 func (f ioFile) Readdir(count int) ([]fs.FileInfo, error) {
791 d, ok := f.file.(fs.ReadDirFile)
792 if !ok {
793 return nil, errMissingReadDir
794 }
795 var list []fs.FileInfo
796 for {
797 dirs, err := d.ReadDir(count - len(list))
798 for _, dir := range dirs {
799 info, err := dir.Info()
800 if err != nil {
801
802 continue
803 }
804 list = append(list, info)
805 }
806 if err != nil {
807 return list, err
808 }
809 if count < 0 || len(list) >= count {
810 break
811 }
812 }
813 return list, nil
814 }
815
816
817
818 func FS(fsys fs.FS) FileSystem {
819 return ioFS{fsys}
820 }
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838 func FileServer(root FileSystem) Handler {
839 return &fileHandler{root}
840 }
841
842 func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) {
843 upath := r.URL.Path
844 if !strings.HasPrefix(upath, "/") {
845 upath = "/" + upath
846 r.URL.Path = upath
847 }
848 serveFile(w, r, f.root, path.Clean(upath), true)
849 }
850
851
852 type httpRange struct {
853 start, length int64
854 }
855
856 func (r httpRange) contentRange(size int64) string {
857 return fmt.Sprintf("bytes %d-%d/%d", r.start, r.start+r.length-1, size)
858 }
859
860 func (r httpRange) mimeHeader(contentType string, size int64) textproto.MIMEHeader {
861 return textproto.MIMEHeader{
862 "Content-Range": {r.contentRange(size)},
863 "Content-Type": {contentType},
864 }
865 }
866
867
868
869 func parseRange(s string, size int64) ([]httpRange, error) {
870 if s == "" {
871 return nil, nil
872 }
873 const b = "bytes="
874 if !strings.HasPrefix(s, b) {
875 return nil, errors.New("invalid range")
876 }
877 var ranges []httpRange
878 noOverlap := false
879 for _, ra := range strings.Split(s[len(b):], ",") {
880 ra = textproto.TrimString(ra)
881 if ra == "" {
882 continue
883 }
884 i := strings.Index(ra, "-")
885 if i < 0 {
886 return nil, errors.New("invalid range")
887 }
888 start, end := textproto.TrimString(ra[:i]), textproto.TrimString(ra[i+1:])
889 var r httpRange
890 if start == "" {
891
892
893
894
895
896 if end == "" || end[0] == '-' {
897 return nil, errors.New("invalid range")
898 }
899 i, err := strconv.ParseInt(end, 10, 64)
900 if i < 0 || err != nil {
901 return nil, errors.New("invalid range")
902 }
903 if i > size {
904 i = size
905 }
906 r.start = size - i
907 r.length = size - r.start
908 } else {
909 i, err := strconv.ParseInt(start, 10, 64)
910 if err != nil || i < 0 {
911 return nil, errors.New("invalid range")
912 }
913 if i >= size {
914
915
916 noOverlap = true
917 continue
918 }
919 r.start = i
920 if end == "" {
921
922 r.length = size - r.start
923 } else {
924 i, err := strconv.ParseInt(end, 10, 64)
925 if err != nil || r.start > i {
926 return nil, errors.New("invalid range")
927 }
928 if i >= size {
929 i = size - 1
930 }
931 r.length = i - r.start + 1
932 }
933 }
934 ranges = append(ranges, r)
935 }
936 if noOverlap && len(ranges) == 0 {
937
938 return nil, errNoOverlap
939 }
940 return ranges, nil
941 }
942
943
944 type countingWriter int64
945
946 func (w *countingWriter) Write(p []byte) (n int, err error) {
947 *w += countingWriter(len(p))
948 return len(p), nil
949 }
950
951
952
953 func rangesMIMESize(ranges []httpRange, contentType string, contentSize int64) (encSize int64) {
954 var w countingWriter
955 mw := multipart.NewWriter(&w)
956 for _, ra := range ranges {
957 mw.CreatePart(ra.mimeHeader(contentType, contentSize))
958 encSize += ra.length
959 }
960 mw.Close()
961 encSize += int64(w)
962 return
963 }
964
965 func sumRangesSize(ranges []httpRange) (size int64) {
966 for _, ra := range ranges {
967 size += ra.length
968 }
969 return
970 }
971
View as plain text