Spaces:
Runtime error
Runtime error
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 | |
} | |