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