package net import ( "fmt" "github.com/alist-org/alist/v3/pkg/utils" "io" "math" "mime/multipart" "net/http" "net/textproto" "strings" "time" "github.com/alist-org/alist/v3/pkg/http_range" log "github.com/sirupsen/logrus" ) // scanETag determines if a syntactically valid ETag is present at s. If so, // the ETag and remaining text after consuming ETag is returned. Otherwise, // it returns "", "". func scanETag(s string) (etag string, remain string) { s = textproto.TrimString(s) start := 0 if strings.HasPrefix(s, "W/") { start = 2 } if len(s[start:]) < 2 || s[start] != '"' { return "", "" } // ETag is either W/"text" or "text". // See RFC 7232 2.3. for i := start + 1; i < len(s); i++ { c := s[i] switch { // Character values allowed in ETags. case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80: case c == '"': return s[:i+1], s[i+1:] default: return "", "" } } return "", "" } // etagStrongMatch reports whether a and b match using strong ETag comparison. // Assumes a and b are valid ETags. func etagStrongMatch(a, b string) bool { return a == b && a != "" && a[0] == '"' } // etagWeakMatch reports whether a and b match using weak ETag comparison. // Assumes a and b are valid ETags. func etagWeakMatch(a, b string) bool { return strings.TrimPrefix(a, "W/") == strings.TrimPrefix(b, "W/") } // condResult is the result of an HTTP request precondition check. // See https://tools.ietf.org/html/rfc7232 section 3. type condResult int const ( condNone condResult = iota condTrue condFalse ) func checkIfMatch(w http.ResponseWriter, r *http.Request) condResult { im := r.Header.Get("If-Match") if im == "" { return condNone } for { im = textproto.TrimString(im) if len(im) == 0 { break } if im[0] == ',' { im = im[1:] continue } if im[0] == '*' { return condTrue } etag, remain := scanETag(im) if etag == "" { break } if etagStrongMatch(etag, w.Header().Get("Etag")) { return condTrue } im = remain } return condFalse } func checkIfUnmodifiedSince(r *http.Request, modtime time.Time) condResult { ius := r.Header.Get("If-Unmodified-Since") if ius == "" || isZeroTime(modtime) { return condNone } t, err := http.ParseTime(ius) if err != nil { return condNone } // The Last-Modified header truncates sub-second precision so // the modtime needs to be truncated too. modtime = modtime.Truncate(time.Second) if ret := modtime.Compare(t); ret <= 0 { return condTrue } return condFalse } func checkIfNoneMatch(w http.ResponseWriter, r *http.Request) condResult { inm := r.Header.Get("If-None-Match") if inm == "" { return condNone } buf := inm for { buf = textproto.TrimString(buf) if len(buf) == 0 { break } if buf[0] == ',' { buf = buf[1:] continue } if buf[0] == '*' { return condFalse } etag, remain := scanETag(buf) if etag == "" { break } if etagWeakMatch(etag, w.Header().Get("Etag")) { return condFalse } buf = remain } return condTrue } func checkIfModifiedSince(r *http.Request, modtime time.Time) condResult { if r.Method != "GET" && r.Method != "HEAD" { return condNone } ims := r.Header.Get("If-Modified-Since") if ims == "" || isZeroTime(modtime) { return condNone } t, err := http.ParseTime(ims) if err != nil { return condNone } // The Last-Modified header truncates sub-second precision so // the modtime needs to be truncated too. modtime = modtime.Truncate(time.Second) if ret := modtime.Compare(t); ret <= 0 { return condFalse } return condTrue } func checkIfRange(w http.ResponseWriter, r *http.Request, modtime time.Time) condResult { if r.Method != "GET" && r.Method != "HEAD" { return condNone } ir := r.Header.Get("If-Range") if ir == "" { return condNone } etag, _ := scanETag(ir) if etag != "" { if etagStrongMatch(etag, w.Header().Get("Etag")) { return condTrue } return condFalse } // The If-Range value is typically the ETag value, but it may also be // the modtime date. See golang.org/issue/8367. if modtime.IsZero() { return condFalse } t, err := http.ParseTime(ir) if err != nil { return condFalse } if t.Unix() == modtime.Unix() { return condTrue } return condFalse } var unixEpochTime = time.Unix(0, 0) // isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0). func isZeroTime(t time.Time) bool { return t.IsZero() || t.Equal(unixEpochTime) } func setLastModified(w http.ResponseWriter, modtime time.Time) { if !isZeroTime(modtime) { w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat)) } } func writeNotModified(w http.ResponseWriter) { // RFC 7232 section 4.1: // a sender SHOULD NOT generate representation metadata other than the // above listed fields unless said metadata exists for the purpose of // guiding cache updates (e.g., Last-Modified might be useful if the // response does not have an ETag field). h := w.Header() delete(h, "Content-Type") delete(h, "Content-Length") delete(h, "Content-Encoding") if h.Get("Etag") != "" { delete(h, "Last-Modified") } w.WriteHeader(http.StatusNotModified) } // checkPreconditions evaluates request preconditions and reports whether a precondition // resulted in sending StatusNotModified or StatusPreconditionFailed. func checkPreconditions(w http.ResponseWriter, r *http.Request, modtime time.Time) (done bool, rangeHeader string) { // This function carefully follows RFC 7232 section 6. ch := checkIfMatch(w, r) if ch == condNone { ch = checkIfUnmodifiedSince(r, modtime) } if ch == condFalse { w.WriteHeader(http.StatusPreconditionFailed) return true, "" } switch checkIfNoneMatch(w, r) { case condFalse: if r.Method == "GET" || r.Method == "HEAD" { writeNotModified(w) return true, "" } w.WriteHeader(http.StatusPreconditionFailed) return true, "" case condNone: if checkIfModifiedSince(r, modtime) == condFalse { writeNotModified(w) return true, "" } } rangeHeader = r.Header.Get("Range") if rangeHeader != "" && checkIfRange(w, r, modtime) == condFalse { rangeHeader = "" } return false, rangeHeader } func sumRangesSize(ranges []http_range.Range) (size int64) { for _, ra := range ranges { size += ra.Length } return } // countingWriter counts how many bytes have been written to it. type countingWriter int64 func (w *countingWriter) Write(p []byte) (n int, err error) { *w += countingWriter(len(p)) return len(p), nil } // rangesMIMESize returns the number of bytes it takes to encode the // provided ranges as a multipart response. func rangesMIMESize(ranges []http_range.Range, contentType string, contentSize int64) (encSize int64, err error) { var w countingWriter mw := multipart.NewWriter(&w) for _, ra := range ranges { _, err := mw.CreatePart(ra.MimeHeader(contentType, contentSize)) if err != nil { return 0, err } encSize += ra.Length } err = mw.Close() if err != nil { return 0, err } encSize += int64(w) return encSize, nil } // LimitedReadCloser wraps a io.ReadCloser and limits the number of bytes that can be read from it. type LimitedReadCloser struct { rc io.ReadCloser remaining int } func (l *LimitedReadCloser) Read(buf []byte) (int, error) { if l.remaining <= 0 { return 0, io.EOF } if len(buf) > l.remaining { buf = buf[0:l.remaining] } n, err := l.rc.Read(buf) l.remaining -= n return n, err } func (l *LimitedReadCloser) Close() error { return l.rc.Close() } // GetRangedHttpReader some http server doesn't support "Range" header, // so this function read readCloser with whole data, skip offset, then return ReaderCloser. func GetRangedHttpReader(readCloser io.ReadCloser, offset, length int64) (io.ReadCloser, error) { var length_int int if length > math.MaxInt { return nil, fmt.Errorf("doesnot support length bigger than int32 max ") } length_int = int(length) if offset > 100*1024*1024 { log.Warnf("offset is more than 100MB, if loading data from internet, high-latency and wasting of bandwidth is expected") } if _, err := utils.CopyWithBuffer(io.Discard, io.LimitReader(readCloser, offset)); err != nil { return nil, err } // return an io.ReadCloser that is limited to `length` bytes. return &LimitedReadCloser{readCloser, length_int}, nil }