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 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, mapOpenError(err, fullName, filepath.Separator, os.Stat)
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, mapOpenError(err, name, '/', func(path string) (fs.FileInfo, error) {
763 return fs.Stat(f.fsys, path)
764 })
765 }
766 return ioFile{file}, nil
767 }
768
769 func (f ioFile) Close() error { return f.file.Close() }
770 func (f ioFile) Read(b []byte) (int, error) { return f.file.Read(b) }
771 func (f ioFile) Stat() (fs.FileInfo, error) { return f.file.Stat() }
772
773 var errMissingSeek = errors.New("io.File missing Seek method")
774 var errMissingReadDir = errors.New("io.File directory missing ReadDir method")
775
776 func (f ioFile) Seek(offset int64, whence int) (int64, error) {
777 s, ok := f.file.(io.Seeker)
778 if !ok {
779 return 0, errMissingSeek
780 }
781 return s.Seek(offset, whence)
782 }
783
784 func (f ioFile) ReadDir(count int) ([]fs.DirEntry, error) {
785 d, ok := f.file.(fs.ReadDirFile)
786 if !ok {
787 return nil, errMissingReadDir
788 }
789 return d.ReadDir(count)
790 }
791
792 func (f ioFile) Readdir(count int) ([]fs.FileInfo, error) {
793 d, ok := f.file.(fs.ReadDirFile)
794 if !ok {
795 return nil, errMissingReadDir
796 }
797 var list []fs.FileInfo
798 for {
799 dirs, err := d.ReadDir(count - len(list))
800 for _, dir := range dirs {
801 info, err := dir.Info()
802 if err != nil {
803
804 continue
805 }
806 list = append(list, info)
807 }
808 if err != nil {
809 return list, err
810 }
811 if count < 0 || len(list) >= count {
812 break
813 }
814 }
815 return list, nil
816 }
817
818
819
820 func FS(fsys fs.FS) FileSystem {
821 return ioFS{fsys}
822 }
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840 func FileServer(root FileSystem) Handler {
841 return &fileHandler{root}
842 }
843
844 func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) {
845 upath := r.URL.Path
846 if !strings.HasPrefix(upath, "/") {
847 upath = "/" + upath
848 r.URL.Path = upath
849 }
850 serveFile(w, r, f.root, path.Clean(upath), true)
851 }
852
853
854 type httpRange struct {
855 start, length int64
856 }
857
858 func (r httpRange) contentRange(size int64) string {
859 return fmt.Sprintf("bytes %d-%d/%d", r.start, r.start+r.length-1, size)
860 }
861
862 func (r httpRange) mimeHeader(contentType string, size int64) textproto.MIMEHeader {
863 return textproto.MIMEHeader{
864 "Content-Range": {r.contentRange(size)},
865 "Content-Type": {contentType},
866 }
867 }
868
869
870
871 func parseRange(s string, size int64) ([]httpRange, error) {
872 if s == "" {
873 return nil, nil
874 }
875 const b = "bytes="
876 if !strings.HasPrefix(s, b) {
877 return nil, errors.New("invalid range")
878 }
879 var ranges []httpRange
880 noOverlap := false
881 for _, ra := range strings.Split(s[len(b):], ",") {
882 ra = textproto.TrimString(ra)
883 if ra == "" {
884 continue
885 }
886 start, end, ok := strings.Cut(ra, "-")
887 if !ok {
888 return nil, errors.New("invalid range")
889 }
890 start, end = textproto.TrimString(start), textproto.TrimString(end)
891 var r httpRange
892 if start == "" {
893
894
895
896
897
898 if end == "" || end[0] == '-' {
899 return nil, errors.New("invalid range")
900 }
901 i, err := strconv.ParseInt(end, 10, 64)
902 if i < 0 || err != nil {
903 return nil, errors.New("invalid range")
904 }
905 if i > size {
906 i = size
907 }
908 r.start = size - i
909 r.length = size - r.start
910 } else {
911 i, err := strconv.ParseInt(start, 10, 64)
912 if err != nil || i < 0 {
913 return nil, errors.New("invalid range")
914 }
915 if i >= size {
916
917
918 noOverlap = true
919 continue
920 }
921 r.start = i
922 if end == "" {
923
924 r.length = size - r.start
925 } else {
926 i, err := strconv.ParseInt(end, 10, 64)
927 if err != nil || r.start > i {
928 return nil, errors.New("invalid range")
929 }
930 if i >= size {
931 i = size - 1
932 }
933 r.length = i - r.start + 1
934 }
935 }
936 ranges = append(ranges, r)
937 }
938 if noOverlap && len(ranges) == 0 {
939
940 return nil, errNoOverlap
941 }
942 return ranges, nil
943 }
944
945
946 type countingWriter int64
947
948 func (w *countingWriter) Write(p []byte) (n int, err error) {
949 *w += countingWriter(len(p))
950 return len(p), nil
951 }
952
953
954
955 func rangesMIMESize(ranges []httpRange, contentType string, contentSize int64) (encSize int64) {
956 var w countingWriter
957 mw := multipart.NewWriter(&w)
958 for _, ra := range ranges {
959 mw.CreatePart(ra.mimeHeader(contentType, contentSize))
960 encSize += ra.length
961 }
962 mw.Close()
963 encSize += int64(w)
964 return
965 }
966
967 func sumRangesSize(ranges []httpRange) (size int64) {
968 for _, ra := range ranges {
969 size += ra.length
970 }
971 return
972 }
973
View as plain text