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