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