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 if w.Header().Get("Content-Encoding") == "" {
347 w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10))
348 }
349
350 w.WriteHeader(code)
351
352 if r.Method != "HEAD" {
353 io.CopyN(w, sendContent, sendSize)
354 }
355 }
356
357
358
359
360 func scanETag(s string) (etag string, remain string) {
361 s = textproto.TrimString(s)
362 start := 0
363 if strings.HasPrefix(s, "W/") {
364 start = 2
365 }
366 if len(s[start:]) < 2 || s[start] != '"' {
367 return "", ""
368 }
369
370
371 for i := start + 1; i < len(s); i++ {
372 c := s[i]
373 switch {
374
375 case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80:
376 case c == '"':
377 return s[:i+1], s[i+1:]
378 default:
379 return "", ""
380 }
381 }
382 return "", ""
383 }
384
385
386
387 func etagStrongMatch(a, b string) bool {
388 return a == b && a != "" && a[0] == '"'
389 }
390
391
392
393 func etagWeakMatch(a, b string) bool {
394 return strings.TrimPrefix(a, "W/") == strings.TrimPrefix(b, "W/")
395 }
396
397
398
399 type condResult int
400
401 const (
402 condNone condResult = iota
403 condTrue
404 condFalse
405 )
406
407 func checkIfMatch(w ResponseWriter, r *Request) condResult {
408 im := r.Header.Get("If-Match")
409 if im == "" {
410 return condNone
411 }
412 for {
413 im = textproto.TrimString(im)
414 if len(im) == 0 {
415 break
416 }
417 if im[0] == ',' {
418 im = im[1:]
419 continue
420 }
421 if im[0] == '*' {
422 return condTrue
423 }
424 etag, remain := scanETag(im)
425 if etag == "" {
426 break
427 }
428 if etagStrongMatch(etag, w.Header().get("Etag")) {
429 return condTrue
430 }
431 im = remain
432 }
433
434 return condFalse
435 }
436
437 func checkIfUnmodifiedSince(r *Request, modtime time.Time) condResult {
438 ius := r.Header.Get("If-Unmodified-Since")
439 if ius == "" || isZeroTime(modtime) {
440 return condNone
441 }
442 t, err := ParseTime(ius)
443 if err != nil {
444 return condNone
445 }
446
447
448
449 modtime = modtime.Truncate(time.Second)
450 if ret := modtime.Compare(t); ret <= 0 {
451 return condTrue
452 }
453 return condFalse
454 }
455
456 func checkIfNoneMatch(w ResponseWriter, r *Request) condResult {
457 inm := r.Header.get("If-None-Match")
458 if inm == "" {
459 return condNone
460 }
461 buf := inm
462 for {
463 buf = textproto.TrimString(buf)
464 if len(buf) == 0 {
465 break
466 }
467 if buf[0] == ',' {
468 buf = buf[1:]
469 continue
470 }
471 if buf[0] == '*' {
472 return condFalse
473 }
474 etag, remain := scanETag(buf)
475 if etag == "" {
476 break
477 }
478 if etagWeakMatch(etag, w.Header().get("Etag")) {
479 return condFalse
480 }
481 buf = remain
482 }
483 return condTrue
484 }
485
486 func checkIfModifiedSince(r *Request, modtime time.Time) condResult {
487 if r.Method != "GET" && r.Method != "HEAD" {
488 return condNone
489 }
490 ims := r.Header.Get("If-Modified-Since")
491 if ims == "" || isZeroTime(modtime) {
492 return condNone
493 }
494 t, err := ParseTime(ims)
495 if err != nil {
496 return condNone
497 }
498
499
500 modtime = modtime.Truncate(time.Second)
501 if ret := modtime.Compare(t); ret <= 0 {
502 return condFalse
503 }
504 return condTrue
505 }
506
507 func checkIfRange(w ResponseWriter, r *Request, modtime time.Time) condResult {
508 if r.Method != "GET" && r.Method != "HEAD" {
509 return condNone
510 }
511 ir := r.Header.get("If-Range")
512 if ir == "" {
513 return condNone
514 }
515 etag, _ := scanETag(ir)
516 if etag != "" {
517 if etagStrongMatch(etag, w.Header().Get("Etag")) {
518 return condTrue
519 } else {
520 return condFalse
521 }
522 }
523
524
525 if modtime.IsZero() {
526 return condFalse
527 }
528 t, err := ParseTime(ir)
529 if err != nil {
530 return condFalse
531 }
532 if t.Unix() == modtime.Unix() {
533 return condTrue
534 }
535 return condFalse
536 }
537
538 var unixEpochTime = time.Unix(0, 0)
539
540
541 func isZeroTime(t time.Time) bool {
542 return t.IsZero() || t.Equal(unixEpochTime)
543 }
544
545 func setLastModified(w ResponseWriter, modtime time.Time) {
546 if !isZeroTime(modtime) {
547 w.Header().Set("Last-Modified", modtime.UTC().Format(TimeFormat))
548 }
549 }
550
551 func writeNotModified(w ResponseWriter) {
552
553
554
555
556
557 h := w.Header()
558 delete(h, "Content-Type")
559 delete(h, "Content-Length")
560 delete(h, "Content-Encoding")
561 if h.Get("Etag") != "" {
562 delete(h, "Last-Modified")
563 }
564 w.WriteHeader(StatusNotModified)
565 }
566
567
568
569 func checkPreconditions(w ResponseWriter, r *Request, modtime time.Time) (done bool, rangeHeader string) {
570
571 ch := checkIfMatch(w, r)
572 if ch == condNone {
573 ch = checkIfUnmodifiedSince(r, modtime)
574 }
575 if ch == condFalse {
576 w.WriteHeader(StatusPreconditionFailed)
577 return true, ""
578 }
579 switch checkIfNoneMatch(w, r) {
580 case condFalse:
581 if r.Method == "GET" || r.Method == "HEAD" {
582 writeNotModified(w)
583 return true, ""
584 } else {
585 w.WriteHeader(StatusPreconditionFailed)
586 return true, ""
587 }
588 case condNone:
589 if checkIfModifiedSince(r, modtime) == condFalse {
590 writeNotModified(w)
591 return true, ""
592 }
593 }
594
595 rangeHeader = r.Header.get("Range")
596 if rangeHeader != "" && checkIfRange(w, r, modtime) == condFalse {
597 rangeHeader = ""
598 }
599 return false, rangeHeader
600 }
601
602
603 func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) {
604 const indexPage = "/index.html"
605
606
607
608
609 if strings.HasSuffix(r.URL.Path, indexPage) {
610 localRedirect(w, r, "./")
611 return
612 }
613
614 f, err := fs.Open(name)
615 if err != nil {
616 msg, code := toHTTPError(err)
617 Error(w, msg, code)
618 return
619 }
620 defer f.Close()
621
622 d, err := f.Stat()
623 if err != nil {
624 msg, code := toHTTPError(err)
625 Error(w, msg, code)
626 return
627 }
628
629 if redirect {
630
631
632 url := r.URL.Path
633 if d.IsDir() {
634 if url[len(url)-1] != '/' {
635 localRedirect(w, r, path.Base(url)+"/")
636 return
637 }
638 } else {
639 if url[len(url)-1] == '/' {
640 localRedirect(w, r, "../"+path.Base(url))
641 return
642 }
643 }
644 }
645
646 if d.IsDir() {
647 url := r.URL.Path
648
649 if url == "" || url[len(url)-1] != '/' {
650 localRedirect(w, r, path.Base(url)+"/")
651 return
652 }
653
654
655 index := strings.TrimSuffix(name, "/") + indexPage
656 ff, err := fs.Open(index)
657 if err == nil {
658 defer ff.Close()
659 dd, err := ff.Stat()
660 if err == nil {
661 d = dd
662 f = ff
663 }
664 }
665 }
666
667
668 if d.IsDir() {
669 if checkIfModifiedSince(r, d.ModTime()) == condFalse {
670 writeNotModified(w)
671 return
672 }
673 setLastModified(w, d.ModTime())
674 dirList(w, r, f)
675 return
676 }
677
678
679 sizeFunc := func() (int64, error) { return d.Size(), nil }
680 serveContent(w, r, d.Name(), d.ModTime(), sizeFunc, f)
681 }
682
683
684
685
686
687
688 func toHTTPError(err error) (msg string, httpStatus int) {
689 if errors.Is(err, fs.ErrNotExist) {
690 return "404 page not found", StatusNotFound
691 }
692 if errors.Is(err, fs.ErrPermission) {
693 return "403 Forbidden", StatusForbidden
694 }
695
696 return "500 Internal Server Error", StatusInternalServerError
697 }
698
699
700
701 func localRedirect(w ResponseWriter, r *Request, newPath string) {
702 if q := r.URL.RawQuery; q != "" {
703 newPath += "?" + q
704 }
705 w.Header().Set("Location", newPath)
706 w.WriteHeader(StatusMovedPermanently)
707 }
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730 func ServeFile(w ResponseWriter, r *Request, name string) {
731 if containsDotDot(r.URL.Path) {
732
733
734
735
736
737 Error(w, "invalid URL path", StatusBadRequest)
738 return
739 }
740 dir, file := filepath.Split(name)
741 serveFile(w, r, Dir(dir), file, false)
742 }
743
744 func containsDotDot(v string) bool {
745 if !strings.Contains(v, "..") {
746 return false
747 }
748 for _, ent := range strings.FieldsFunc(v, isSlashRune) {
749 if ent == ".." {
750 return true
751 }
752 }
753 return false
754 }
755
756 func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
757
758 type fileHandler struct {
759 root FileSystem
760 }
761
762 type ioFS struct {
763 fsys fs.FS
764 }
765
766 type ioFile struct {
767 file fs.File
768 }
769
770 func (f ioFS) Open(name string) (File, error) {
771 if name == "/" {
772 name = "."
773 } else {
774 name = strings.TrimPrefix(name, "/")
775 }
776 file, err := f.fsys.Open(name)
777 if err != nil {
778 return nil, mapOpenError(err, name, '/', func(path string) (fs.FileInfo, error) {
779 return fs.Stat(f.fsys, path)
780 })
781 }
782 return ioFile{file}, nil
783 }
784
785 func (f ioFile) Close() error { return f.file.Close() }
786 func (f ioFile) Read(b []byte) (int, error) { return f.file.Read(b) }
787 func (f ioFile) Stat() (fs.FileInfo, error) { return f.file.Stat() }
788
789 var errMissingSeek = errors.New("io.File missing Seek method")
790 var errMissingReadDir = errors.New("io.File directory missing ReadDir method")
791
792 func (f ioFile) Seek(offset int64, whence int) (int64, error) {
793 s, ok := f.file.(io.Seeker)
794 if !ok {
795 return 0, errMissingSeek
796 }
797 return s.Seek(offset, whence)
798 }
799
800 func (f ioFile) ReadDir(count int) ([]fs.DirEntry, error) {
801 d, ok := f.file.(fs.ReadDirFile)
802 if !ok {
803 return nil, errMissingReadDir
804 }
805 return d.ReadDir(count)
806 }
807
808 func (f ioFile) Readdir(count int) ([]fs.FileInfo, error) {
809 d, ok := f.file.(fs.ReadDirFile)
810 if !ok {
811 return nil, errMissingReadDir
812 }
813 var list []fs.FileInfo
814 for {
815 dirs, err := d.ReadDir(count - len(list))
816 for _, dir := range dirs {
817 info, err := dir.Info()
818 if err != nil {
819
820 continue
821 }
822 list = append(list, info)
823 }
824 if err != nil {
825 return list, err
826 }
827 if count < 0 || len(list) >= count {
828 break
829 }
830 }
831 return list, nil
832 }
833
834
835
836
837 func FS(fsys fs.FS) FileSystem {
838 return ioFS{fsys}
839 }
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856 func FileServer(root FileSystem) Handler {
857 return &fileHandler{root}
858 }
859
860 func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) {
861 upath := r.URL.Path
862 if !strings.HasPrefix(upath, "/") {
863 upath = "/" + upath
864 r.URL.Path = upath
865 }
866 serveFile(w, r, f.root, path.Clean(upath), true)
867 }
868
869
870 type httpRange struct {
871 start, length int64
872 }
873
874 func (r httpRange) contentRange(size int64) string {
875 return fmt.Sprintf("bytes %d-%d/%d", r.start, r.start+r.length-1, size)
876 }
877
878 func (r httpRange) mimeHeader(contentType string, size int64) textproto.MIMEHeader {
879 return textproto.MIMEHeader{
880 "Content-Range": {r.contentRange(size)},
881 "Content-Type": {contentType},
882 }
883 }
884
885
886
887 func parseRange(s string, size int64) ([]httpRange, error) {
888 if s == "" {
889 return nil, nil
890 }
891 const b = "bytes="
892 if !strings.HasPrefix(s, b) {
893 return nil, errors.New("invalid range")
894 }
895 var ranges []httpRange
896 noOverlap := false
897 for _, ra := range strings.Split(s[len(b):], ",") {
898 ra = textproto.TrimString(ra)
899 if ra == "" {
900 continue
901 }
902 start, end, ok := strings.Cut(ra, "-")
903 if !ok {
904 return nil, errors.New("invalid range")
905 }
906 start, end = textproto.TrimString(start), textproto.TrimString(end)
907 var r httpRange
908 if start == "" {
909
910
911
912
913
914 if end == "" || end[0] == '-' {
915 return nil, errors.New("invalid range")
916 }
917 i, err := strconv.ParseInt(end, 10, 64)
918 if i < 0 || err != nil {
919 return nil, errors.New("invalid range")
920 }
921 if i > size {
922 i = size
923 }
924 r.start = size - i
925 r.length = size - r.start
926 } else {
927 i, err := strconv.ParseInt(start, 10, 64)
928 if err != nil || i < 0 {
929 return nil, errors.New("invalid range")
930 }
931 if i >= size {
932
933
934 noOverlap = true
935 continue
936 }
937 r.start = i
938 if end == "" {
939
940 r.length = size - r.start
941 } else {
942 i, err := strconv.ParseInt(end, 10, 64)
943 if err != nil || r.start > i {
944 return nil, errors.New("invalid range")
945 }
946 if i >= size {
947 i = size - 1
948 }
949 r.length = i - r.start + 1
950 }
951 }
952 ranges = append(ranges, r)
953 }
954 if noOverlap && len(ranges) == 0 {
955
956 return nil, errNoOverlap
957 }
958 return ranges, nil
959 }
960
961
962 type countingWriter int64
963
964 func (w *countingWriter) Write(p []byte) (n int, err error) {
965 *w += countingWriter(len(p))
966 return len(p), nil
967 }
968
969
970
971 func rangesMIMESize(ranges []httpRange, contentType string, contentSize int64) (encSize int64) {
972 var w countingWriter
973 mw := multipart.NewWriter(&w)
974 for _, ra := range ranges {
975 mw.CreatePart(ra.mimeHeader(contentType, contentSize))
976 encSize += ra.length
977 }
978 mw.Close()
979 encSize += int64(w)
980 return
981 }
982
983 func sumRangesSize(ranges []httpRange) (size int64) {
984 for _, ra := range ranges {
985 size += ra.length
986 }
987 return
988 }
989
View as plain text