mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2026-10-10 04:53:09 +08:00
fix(webdav): honor conditional PUT requests
- Reuse HTTP precondition checks for existing and missing upload targets - Support repeated entity-tag headers and reject empty If-Match lists - Preserve nanosecond precision in metadata-based ETags - Return 204 for overwrites and retain ETags in buffered PUT responses - Add regression tests for preconditions, ranges, and ETag changes Co-authored-by: Codex <267193182+codex@users.noreply.github.com> Signed-off-by: jyxjjj <16695261+jyxjjj@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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, exists bool) condResult {
|
||||
values := r.Header.Values("If-Match")
|
||||
if len(values) == 0 {
|
||||
return condNone
|
||||
}
|
||||
im := strings.Join(values, ",")
|
||||
r.Header.Del("If-Match")
|
||||
if !exists {
|
||||
return condFalse
|
||||
}
|
||||
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 == "" {
|
||||
return condNone
|
||||
}
|
||||
r.Header.Del("If-Unmodified-Since")
|
||||
if 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, exists bool) condResult {
|
||||
values := r.Header.Values("If-None-Match")
|
||||
if len(values) == 0 {
|
||||
return condNone
|
||||
}
|
||||
inm := strings.Join(values, ",")
|
||||
r.Header.Del("If-None-Match")
|
||||
if !exists {
|
||||
return condTrue
|
||||
}
|
||||
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 == "" {
|
||||
return condNone
|
||||
}
|
||||
r.Header.Del("If-Modified-Since")
|
||||
if 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
|
||||
}
|
||||
r.Header.Del("If-Range")
|
||||
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. The caller must
|
||||
// set the current ETag response header and indicate whether the representation exists.
|
||||
func CheckPreconditions(w http.ResponseWriter, r *http.Request, modtime time.Time, exists bool) (done bool, rangeHeader string) {
|
||||
// Evaluate preconditions in the order specified by RFC 9110 section 13.2.2.
|
||||
ch := checkIfMatch(w, r, exists)
|
||||
if ch == condNone {
|
||||
ch = checkIfUnmodifiedSince(r, modtime)
|
||||
}
|
||||
if ch == condFalse {
|
||||
w.WriteHeader(http.StatusPreconditionFailed)
|
||||
return true, ""
|
||||
}
|
||||
switch checkIfNoneMatch(w, r, exists) {
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCheckPreconditions(t *testing.T) {
|
||||
modified := time.Date(2026, time.January, 2, 3, 4, 5, 123456789, time.UTC)
|
||||
date := modified.Format(http.TimeFormat)
|
||||
older := modified.Add(-time.Second).Format(http.TimeFormat)
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
method string
|
||||
exists bool
|
||||
etag string
|
||||
header http.Header
|
||||
status int
|
||||
}{
|
||||
{"unconditional create", "PUT", false, "", nil, 0},
|
||||
{"unconditional overwrite", "PUT", true, `"current"`, nil, 0},
|
||||
{"create only missing", "PUT", false, "", http.Header{"If-None-Match": {"*"}}, 0},
|
||||
{"create only existing", "PUT", true, `"current"`, http.Header{"If-None-Match": {"*"}}, 412},
|
||||
{"create only existing without etag", "PUT", true, "", http.Header{"If-None-Match": {"*"}}, 412},
|
||||
{"match missing", "PUT", false, "", http.Header{"If-Match": {"*"}}, 412},
|
||||
{"match existing", "PUT", true, `"current"`, http.Header{"If-Match": {"*"}}, 0},
|
||||
{"match existing without etag", "PUT", true, "", http.Header{"If-Match": {"*"}}, 0},
|
||||
{"match current", "PUT", true, `"current"`, http.Header{"If-Match": {`"current"`}}, 0},
|
||||
{"match stale", "PUT", true, `"current"`, http.Header{"If-Match": {`"stale"`}}, 412},
|
||||
{"match missing tag", "PUT", false, "", http.Header{"If-Match": {`"current"`}}, 412},
|
||||
{"match weak request", "PUT", true, `"current"`, http.Header{"If-Match": {`W/"current"`}}, 412},
|
||||
{"match weak representation", "PUT", true, `W/"current"`, http.Header{"If-Match": {`"current"`}}, 412},
|
||||
{"match list", "PUT", true, `"current"`, http.Header{"If-Match": {`"stale", "current"`}}, 0},
|
||||
{"match multiple lines", "PUT", true, `"current"`, http.Header{"If-Match": {`"stale"`, `"current"`}}, 0},
|
||||
{"match empty list members", "PUT", true, `"current"`, http.Header{"If-Match": {`, , "current",`}}, 0},
|
||||
{"match quoted comma", "PUT", true, `"one,two"`, http.Header{"If-Match": {`"stale", "one,two"`}}, 0},
|
||||
{"malformed match", "PUT", true, `"current"`, http.Header{"If-Match": {"current"}}, 412},
|
||||
{"empty match", "PUT", true, `"current"`, http.Header{"If-Match": {""}}, 412},
|
||||
{"none match current", "PUT", true, `"current"`, http.Header{"If-None-Match": {`"current"`}}, 412},
|
||||
{"none match stale", "PUT", true, `"current"`, http.Header{"If-None-Match": {`"stale"`}}, 0},
|
||||
{"none match missing tag", "PUT", false, "", http.Header{"If-None-Match": {`"current"`}}, 0},
|
||||
{"none match weak request", "PUT", true, `"current"`, http.Header{"If-None-Match": {`W/"current"`}}, 412},
|
||||
{"none match weak representation", "PUT", true, `W/"current"`, http.Header{"If-None-Match": {`"current"`}}, 412},
|
||||
{"none match multiple lines", "PUT", true, `"current"`, http.Header{"If-None-Match": {`"stale"`, `"current"`}}, 412},
|
||||
{"none match quoted comma", "PUT", true, `"one,two"`, http.Header{"If-None-Match": {`"stale", "one,two"`}}, 412},
|
||||
{"both conditions must pass", "PUT", true, `"current"`, http.Header{"If-Match": {`"current"`}, "If-None-Match": {`"current"`}}, 412},
|
||||
{"both conditions pass", "PUT", true, `"current"`, http.Header{"If-Match": {`"current"`}, "If-None-Match": {`"stale"`}}, 0},
|
||||
{"both wildcards missing", "PUT", false, "", http.Header{"If-Match": {"*"}, "If-None-Match": {"*"}}, 412},
|
||||
{"unmodified since older", "PUT", true, `"current"`, http.Header{"If-Unmodified-Since": {older}}, 412},
|
||||
{"unmodified since same second", "PUT", true, `"current"`, http.Header{"If-Unmodified-Since": {date}}, 0},
|
||||
{"unmodified since missing", "PUT", false, "", http.Header{"If-Unmodified-Since": {older}}, 0},
|
||||
{"invalid unmodified since", "PUT", true, `"current"`, http.Header{"If-Unmodified-Since": {"invalid"}}, 0},
|
||||
{"match overrides unmodified since", "PUT", true, `"current"`, http.Header{"If-Match": {`"current"`}, "If-Unmodified-Since": {older}}, 0},
|
||||
{"ignore modified since on put", "PUT", true, `"current"`, http.Header{"If-Modified-Since": {date}}, 0},
|
||||
{"get not modified", "GET", true, `"current"`, http.Header{"If-None-Match": {`W/"current"`}}, 304},
|
||||
{"head not modified", "HEAD", true, `"current"`, http.Header{"If-None-Match": {"*"}}, 304},
|
||||
{"get stale match", "GET", true, `"current"`, http.Header{"If-Match": {`"stale"`}}, 412},
|
||||
{"get modified since", "GET", true, `"current"`, http.Header{"If-Modified-Since": {date}}, 304},
|
||||
{"none match overrides modified since", "GET", true, `"current"`, http.Header{"If-None-Match": {`"stale"`}, "If-Modified-Since": {date}}, 0},
|
||||
{"empty none match overrides modified since", "GET", true, `"current"`, http.Header{"If-None-Match": {""}, "If-Modified-Since": {date}}, 0},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := httptest.NewRequest(tt.method, "/file.txt", nil)
|
||||
for name, values := range tt.header {
|
||||
for _, value := range values {
|
||||
r.Header.Add(name, value)
|
||||
}
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
if tt.etag != "" {
|
||||
w.Header().Set("Etag", tt.etag)
|
||||
}
|
||||
var modTime time.Time
|
||||
if tt.exists {
|
||||
modTime = modified
|
||||
}
|
||||
done, _ := CheckPreconditions(w, r, modTime, tt.exists)
|
||||
if done != (tt.status != 0) {
|
||||
t.Fatalf("done = %v, want status %d", done, tt.status)
|
||||
}
|
||||
if done && w.Code != tt.status {
|
||||
t.Errorf("status = %d, want %d", w.Code, tt.status)
|
||||
}
|
||||
if w.Body.Len() != 0 {
|
||||
t.Errorf("unexpected response body: %q", w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPreconditionsIfRange(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
etag string
|
||||
want string
|
||||
}{
|
||||
{`"current"`, "bytes=0-3"},
|
||||
{`"stale"`, ""},
|
||||
{`W/"current"`, ""},
|
||||
} {
|
||||
t.Run(tt.etag, func(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodGet, "/file.txt", nil)
|
||||
r.Header.Set("Range", "bytes=0-3")
|
||||
r.Header.Set("If-Range", tt.etag)
|
||||
w := httptest.NewRecorder()
|
||||
w.Header().Set("Etag", `"current"`)
|
||||
done, got := CheckPreconditions(w, r, time.Time{}, true)
|
||||
if done || got != tt.want {
|
||||
t.Fatalf("CheckPreconditions() = (%v, %q), want (false, %q)", done, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ import (
|
||||
func ServeHTTP(w http.ResponseWriter, r *http.Request, name string, modTime time.Time, size int64, RangeReadCloser model.RangeReadCloserIF) error {
|
||||
defer RangeReadCloser.Close()
|
||||
setLastModified(w, modTime)
|
||||
done, rangeReq := checkPreconditions(w, r, modTime)
|
||||
done, rangeReq := CheckPreconditions(w, r, modTime, true)
|
||||
if done {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,10 +4,7 @@ import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/conf"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
@@ -18,260 +15,6 @@ import (
|
||||
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
|
||||
}
|
||||
r.Header.Del("If-Match")
|
||||
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 == "" {
|
||||
return condNone
|
||||
}
|
||||
r.Header.Del("If-Unmodified-Since")
|
||||
if 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
|
||||
}
|
||||
r.Header.Del("If-None-Match")
|
||||
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 == "" {
|
||||
return condNone
|
||||
}
|
||||
r.Header.Del("If-Modified-Since")
|
||||
if 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
|
||||
}
|
||||
r.Header.Del("If-Range")
|
||||
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
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
)
|
||||
|
||||
func TestGetEtagSubsecondChanges(t *testing.T) {
|
||||
file := &model.Object{Size: 4, Modified: time.Unix(1700000000, 100)}
|
||||
before := GetEtag(file, file.Size)
|
||||
if got := GetEtag(file, file.Size); got != before {
|
||||
t.Fatalf("unchanged file ETag = %q, want %q", got, before)
|
||||
}
|
||||
file.Modified = file.Modified.Add(time.Nanosecond)
|
||||
if got := GetEtag(file, file.Size); got == before {
|
||||
t.Errorf("same-size overwrite within one second retained ETag %q", got)
|
||||
}
|
||||
file.Modified = file.Modified.Add(-time.Nanosecond)
|
||||
if got := GetEtag(file, file.Size+1); got == before {
|
||||
t.Errorf("changed size retained ETag %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEtagContentHash(t *testing.T) {
|
||||
file := &model.Object{
|
||||
Size: 4,
|
||||
HashInfo: utils.NewHashInfo(utils.SHA256, utils.HashData(utils.SHA256, []byte("data"))),
|
||||
}
|
||||
before := GetEtag(file, file.Size)
|
||||
file.Modified = time.Unix(1700000000, 100)
|
||||
if got := GetEtag(file, file.Size); got != before {
|
||||
t.Errorf("unchanged content ETag = %q, want %q", got, before)
|
||||
}
|
||||
file.HashInfo = utils.NewHashInfo(utils.SHA256, utils.HashData(utils.SHA256, []byte("next")))
|
||||
if got := GetEtag(file, file.Size); got == before {
|
||||
t.Errorf("changed content retained ETag %q", got)
|
||||
}
|
||||
}
|
||||
@@ -97,8 +97,8 @@ func GetEtag(file model.Obj, size int64) string {
|
||||
if len(hash) > 0 {
|
||||
return fmt.Sprintf(`"%s"`, hash)
|
||||
}
|
||||
// 参考nginx
|
||||
return fmt.Sprintf(`"%x-%x"`, file.ModTime().Unix(), size)
|
||||
// Preserve sub-second changes when files of the same size are overwritten.
|
||||
return fmt.Sprintf(`"%x-%x"`, file.ModTime().UnixNano(), size)
|
||||
}
|
||||
|
||||
func ProxyRange(ctx context.Context, link *model.Link, size int64) *model.Link {
|
||||
|
||||
+32
-5
@@ -355,8 +355,6 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int,
|
||||
return status, err
|
||||
}
|
||||
defer release()
|
||||
// TODO(rost): Support the If-Match, If-None-Match headers? See bradfitz'
|
||||
// comments in http.checkEtag.
|
||||
ctx := r.Context()
|
||||
user := ctx.Value(conf.UserKey).(*model.User)
|
||||
reqPath, err = user.JoinPath(reqPath)
|
||||
@@ -394,6 +392,30 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int,
|
||||
if !common.CanWrite(user, parentMeta, parentPath) {
|
||||
return http.StatusForbidden, errs.PermissionDenied
|
||||
}
|
||||
fi, err := fs.Get(ctx, reqPath, &fs.GetArgs{})
|
||||
exists := err == nil
|
||||
if err != nil && !errs.IsObjectNotFound(err) {
|
||||
if errs.IsNotFoundError(err) {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
var modTime time.Time
|
||||
if exists {
|
||||
if fi.IsDir() {
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
etag, err := findETag(ctx, h.LockSystem, reqPath, fi)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
w.Header().Set("Etag", etag)
|
||||
modTime = fi.ModTime()
|
||||
}
|
||||
if done, _ := net.CheckPreconditions(w, r, modTime, exists); done {
|
||||
return 0, nil
|
||||
}
|
||||
w.Header().Del("Etag")
|
||||
fsStream := &stream.FileStream{
|
||||
Obj: &obj,
|
||||
Reader: r.Body,
|
||||
@@ -411,16 +433,21 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int,
|
||||
if err != nil {
|
||||
return http.StatusMethodNotAllowed, err
|
||||
}
|
||||
fi, err := fs.Get(ctx, reqPath, &fs.GetArgs{})
|
||||
fi, err = fs.Get(ctx, reqPath, &fs.GetArgs{})
|
||||
if err != nil {
|
||||
fi = &obj
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
etag, err := findETag(ctx, h.LockSystem, reqPath, fi)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
w.Header().Set("Etag", etag)
|
||||
return http.StatusCreated, nil
|
||||
if exists {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request) (status int, err error) {
|
||||
|
||||
Reference in New Issue
Block a user