mirror of https://github.com/Xhofe/alist
342 lines
8.1 KiB
Go
342 lines
8.1 KiB
Go
package net
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/textproto"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/alist-org/alist/v3/pkg/utils"
|
|
|
|
"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
|
|
}
|