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