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