fix(nc/webdav): honour If-Match / If-None-Match on PUT (RFC 7232)
Closes F5/F6. The NC PUT handler now evaluates conditional preconditions before body ingestion and returns 412 Precondition Failed when they fail: - If-None-Match: * on an existing target → 412 (create-if-absent) - If-None-Match with matching ETag → 412 (weak compare) - If-Match: * with no current representation → 412 - If-Match with no listed ETag strong-matching the current → 412 The lookup that drives the precondition reuses the same query the handler already needed for the 201-vs-204 distinction, so this adds no extra DB round-trip. Rejected requests skip body ingestion entirely so a 412 doesn't waste megabytes of bandwidth + disk I/O. Test F5/F6 flipped from 'pinned current 204' to assert 412, plus mirror cases F5b/F6b/F6c/F6d covering the legitimate-success paths so logical-operator regressions can't slip past silently.
This commit is contained in:
@@ -145,27 +145,83 @@ ACTUAL=$(nc_curl "$NC_FILES_BASE/f1-small.txt")
|
||||
pass "F4: GET after overwrite serves the new bytes (no stale-cache)"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# F5 / F6 — Conditional PUT (pinned: currently no-op)
|
||||
# F5 / F6 — Conditional PUT (RFC 7232 §3.1/§3.2, RFC 4918 §10)
|
||||
#
|
||||
# F5 covers `If-None-Match: *`: server MUST refuse the PUT with
|
||||
# 412 when the target representation already exists (used by
|
||||
# clients to do "create only if absent"). The mirror case — same
|
||||
# header on a NEW path — must succeed; covered by F5b.
|
||||
#
|
||||
# F6 covers `If-Match: "<etag>"`: server MUST refuse the PUT with
|
||||
# 412 when the supplied ETag doesn't strong-match the current
|
||||
# representation (used by clients to do "update only if
|
||||
# unchanged"). The mirror case — correct ETag → success — is
|
||||
# covered by F6b.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
echo " F5: PUT with If-None-Match: * on existing path (pinned current: 204, RFC-4918 would be 412)"
|
||||
echo " F5: PUT with If-None-Match: * on existing path → 412"
|
||||
HEADERS=$(nc_curl -D - -o /dev/null -X PUT \
|
||||
-H "If-None-Match: *" -H "Content-Type: text/plain" \
|
||||
--data-binary 'F5-payload' \
|
||||
"$NC_FILES_BASE/f1-small.txt")
|
||||
STATUS=$(awk 'NR==1{print $2}' <<< "$HEADERS" | tr -d '\r')
|
||||
[[ "$STATUS" == "204" || "$STATUS" == "201" ]] \
|
||||
|| fail "F5: unexpected status $STATUS (expected 204 — current ignore-conditional behaviour)"
|
||||
pass "F5: PUT honours no conditional headers today — pinned"
|
||||
[[ "$STATUS" == "412" ]] \
|
||||
|| fail "F5: expected 412 Precondition Failed for If-None-Match: * on existing path, got $STATUS"
|
||||
pass "F5: If-None-Match: * on existing path → 412"
|
||||
|
||||
echo " F6: PUT with If-Match: \"wrong-etag\" (pinned current: succeeds, RFC-4918 would be 412)"
|
||||
echo " F5b: PUT with If-None-Match: * on NEW path → 201/204"
|
||||
HEADERS=$(nc_curl -D - -o /dev/null -X PUT \
|
||||
-H "If-None-Match: *" -H "Content-Type: text/plain" \
|
||||
--data-binary 'F5b-payload' \
|
||||
"$NC_FILES_BASE/f5b-new.txt")
|
||||
STATUS=$(awk 'NR==1{print $2}' <<< "$HEADERS" | tr -d '\r')
|
||||
[[ "$STATUS" == "201" || "$STATUS" == "204" ]] \
|
||||
|| fail "F5b: expected 201/204 for If-None-Match: * on new path, got $STATUS"
|
||||
pass "F5b: If-None-Match: * on new path → $STATUS"
|
||||
|
||||
echo " F6: PUT with If-Match: \"wrong-etag\" → 412"
|
||||
HEADERS=$(nc_curl -D - -o /dev/null -X PUT \
|
||||
-H 'If-Match: "deadbeef-never-matches"' -H "Content-Type: text/plain" \
|
||||
--data-binary 'F6-payload' \
|
||||
"$NC_FILES_BASE/f1-small.txt")
|
||||
STATUS=$(awk 'NR==1{print $2}' <<< "$HEADERS" | tr -d '\r')
|
||||
[[ "$STATUS" == "204" || "$STATUS" == "201" ]] \
|
||||
|| fail "F6: unexpected status $STATUS (expected 204 — current ignore-conditional behaviour)"
|
||||
pass "F6: PUT honours no If-Match today — pinned"
|
||||
[[ "$STATUS" == "412" ]] \
|
||||
|| fail "F6: expected 412 Precondition Failed for non-matching If-Match, got $STATUS"
|
||||
pass "F6: If-Match with non-matching ETag → 412"
|
||||
|
||||
echo " F6b: PUT with correct If-Match → 204"
|
||||
# Fetch the current ETag of f1-small.txt via PROPFIND-ish HEAD,
|
||||
# then re-PUT with that exact value as If-Match. Must succeed.
|
||||
CURRENT_ETAG=$(nc_curl -D - -o /dev/null -X HEAD "$NC_FILES_BASE/f1-small.txt" \
|
||||
| awk 'BEGIN{IGNORECASE=1} /^etag:/ {print $2}' | tr -d '\r')
|
||||
[[ -n "$CURRENT_ETAG" ]] || fail "F6b: could not read current ETag via HEAD"
|
||||
HEADERS=$(nc_curl -D - -o /dev/null -X PUT \
|
||||
-H "If-Match: $CURRENT_ETAG" -H "Content-Type: text/plain" \
|
||||
--data-binary 'F6b-payload' \
|
||||
"$NC_FILES_BASE/f1-small.txt")
|
||||
STATUS=$(awk 'NR==1{print $2}' <<< "$HEADERS" | tr -d '\r')
|
||||
[[ "$STATUS" == "204" ]] \
|
||||
|| fail "F6b: expected 204 for If-Match with correct ETag, got $STATUS"
|
||||
pass "F6b: If-Match with current ETag → 204"
|
||||
|
||||
echo " F6c: PUT with If-Match: * on existing path → 204 (catch-all)"
|
||||
HEADERS=$(nc_curl -D - -o /dev/null -X PUT \
|
||||
-H 'If-Match: *' -H "Content-Type: text/plain" \
|
||||
--data-binary 'F6c-payload' \
|
||||
"$NC_FILES_BASE/f1-small.txt")
|
||||
STATUS=$(awk 'NR==1{print $2}' <<< "$HEADERS" | tr -d '\r')
|
||||
[[ "$STATUS" == "204" ]] \
|
||||
|| fail "F6c: expected 204 for If-Match: * on existing path, got $STATUS"
|
||||
pass "F6c: If-Match: * on existing path → 204"
|
||||
|
||||
echo " F6d: PUT with If-Match on NEW path → 412 (resource absent → cannot match)"
|
||||
HEADERS=$(nc_curl -D - -o /dev/null -X PUT \
|
||||
-H 'If-Match: "anything"' -H "Content-Type: text/plain" \
|
||||
--data-binary 'F6d-payload' \
|
||||
"$NC_FILES_BASE/f6d-new.txt")
|
||||
STATUS=$(awk 'NR==1{print $2}' <<< "$HEADERS" | tr -d '\r')
|
||||
[[ "$STATUS" == "412" ]] \
|
||||
|| fail "F6d: expected 412 for If-Match on absent path, got $STATUS"
|
||||
pass "F6d: If-Match on absent path → 412"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# F7 — PUT a "large" file → succeeds, GET returns exact bytes
|
||||
|
||||
Reference in New Issue
Block a user