Author: jpeck
Date: Sat Aug 15 21:26:59 2026
New Revision: 1937142
Log:
Follow-up to r1936010: close the remaining issue #3445 residual in the
write-through proxy.
The body-rewrite filters still did a blind substitution of the location root,
so a dead-property value containing the master location, or a mid-path directory
named like the slave location, was rewritten along with the genuine hrefs.
Anchor the match on the href tag (<D:href>): serf and mod_dav(_svn) both emit
that prefix with the path immediately after the tag, so only href-initial roots
are translated. A raw <D:href> inside a property value would still be
rewritten;, that cannot currently happen because the emitters escape or
base64-encode values. Although that is not a structural guarantee.
* subversion/mod_dav_svn/mirror.c
(PROXY_HREF_ANCHOR): New. Document the href-tag anchor.
(locate_ctx_t): Store search/replace strings instead of named local/remote
paths.
(locate_ctx_init): New. Build the anchored pattern and replacement from a
from-root and a to-root.
(dav_svn__location_in_filter, dav_svn__location_body_filter): Use
locate_ctx_init.
(dav_svn__location_header_filter): Concatenate the slave root and the
remainder without a separator, avoiding a doubled slash.
* subversion/tests/cmdline/dav-mirror-autocheck.sh
(curl_auth, create_master_txn, delete_master_txn, propfind_slave_txr): New
helpers for the in-txn curl probes.
Add full coverage of all routes through the proxy: move and v1
MKACTIVITY/CHECKOUT
including a clean Location rewrite. A proxied revprop change whose value
contains
the slave URL, dead-property values surviving a proxied PROPFIND (the former
XFAIL),
and a location-name collision in multistatus hrefs. Tests now all include
<D:href>
with the slave URL in the user data to ensure they are escaped properly or
protected by the filter whitelist.
Modified:
subversion/branches/proxy-move-copy-fix/subversion/mod_dav_svn/mirror.c
subversion/branches/proxy-move-copy-fix/subversion/tests/cmdline/dav-mirror-autocheck.sh
Modified:
subversion/branches/proxy-move-copy-fix/subversion/mod_dav_svn/mirror.c
==============================================================================
--- subversion/branches/proxy-move-copy-fix/subversion/mod_dav_svn/mirror.c
Sat Aug 15 13:39:08 2026 (r1937141)
+++ subversion/branches/proxy-move-copy-fix/subversion/mod_dav_svn/mirror.c
Sat Aug 15 21:26:59 2026 (r1937142)
@@ -32,6 +32,18 @@
#include "dav_svn.h"
+/* The rewrite anchor: hrefs in the protocol bodies we filter always
+ appear as <D:href>/path -- serf and mod_dav(_svn) both hard-code the
+ "D:" prefix and emit the value immediately after the tag. Anchoring
+ the match here means only href-initial location roots are translated;
+ mid-path components and property values pass through.
+
+ A raw "<D:href>" inside a property value would still be rewritten.
+ That cannot currently happen because the emitters escape or
+ base64-encode values (deadprops.c). Although this is not a structural
+ guarantee. */
+#define PROXY_HREF_ANCHOR "<D:href>"
+
/* If the request carries a Destination header (as COPY and MOVE do), rewrite
it to target the master server instead of this slave. MASTER_URI is the
@@ -207,12 +219,29 @@ typedef struct locate_ctx_t
{
const apr_strmatch_pattern *pattern;
apr_size_t pattern_len;
- const char *localpath;
- apr_size_t localpath_len;
- const char *remotepath;
- apr_size_t remotepath_len;
+ const char *replacement;
+ apr_size_t replacement_len;
} locate_ctx_t;
+/* Initialize CTX to rewrite href-initial FROM_ROOT into TO_ROOT.
+ Both roots are already canonical and URI-encoded, the same domain
+ the protocol bodies use on the wire. */
+static void
+locate_ctx_init(locate_ctx_t *ctx,
+ apr_pool_t *pool,
+ const char *from_root,
+ const char *to_root)
+{
+ const char *from;
+
+ from = apr_pstrcat(pool, PROXY_HREF_ANCHOR, from_root, SVN_VA_NULL);
+ ctx->replacement = apr_pstrcat(pool, PROXY_HREF_ANCHOR, to_root,
+ SVN_VA_NULL);
+ ctx->replacement_len = strlen(ctx->replacement);
+ ctx->pattern = apr_strmatch_precompile(pool, from, 1);
+ ctx->pattern_len = strlen(from);
+}
+
apr_status_t dav_svn__location_in_filter(ap_filter_t *f,
apr_bucket_brigade *bb,
ap_input_mode_t mode,
@@ -244,18 +273,9 @@ apr_status_t dav_svn__location_in_filter
return ap_get_brigade(f->next, bb, mode, block, readbytes);
}
- /* Both CANONICALIZED_URI and ROOT_DIR are already canonical and
- URI-encoded (svn_urlpath__canonicalize() output and the stored
- <Location> path, respectively), which is the same domain the
- protocol bodies use on the wire. */
if (!f->ctx) {
ctx = f->ctx = apr_pcalloc(r->pool, sizeof(*ctx));
- ctx->remotepath = canonicalized_uri;
- ctx->remotepath_len = strlen(ctx->remotepath);
- ctx->localpath = root_dir;
- ctx->localpath_len = strlen(ctx->localpath);
- ctx->pattern = apr_strmatch_precompile(r->pool, ctx->localpath, 1);
- ctx->pattern_len = ctx->localpath_len;
+ locate_ctx_init(ctx, r->pool, root_dir, canonicalized_uri);
}
rv = ap_get_brigade(f->next, bb, mode, block, readbytes);
@@ -284,8 +304,8 @@ apr_status_t dav_svn__location_in_filter
apr_bucket_split(next_bucket, ctx->pattern_len);
bkt = APR_BUCKET_NEXT(next_bucket);
apr_bucket_delete(next_bucket);
- next_bucket = apr_bucket_pool_create(ctx->remotepath,
- ctx->remotepath_len,
+ next_bucket = apr_bucket_pool_create(ctx->replacement,
+ ctx->replacement_len,
r->pool, bb->bucket_alloc);
APR_BUCKET_INSERT_BEFORE(bkt, next_bucket);
}
@@ -301,7 +321,7 @@ apr_status_t dav_svn__location_header_fi
{
request_rec *r = f->r;
const char *master_uri;
- const char *location, *start_foo = NULL;
+ const char *location, *remainder = NULL;
/* Don't filter if we're in a subrequest or we aren't setup to
proxy anything. */
@@ -313,15 +333,18 @@ apr_status_t dav_svn__location_header_fi
location = apr_table_get(r->headers_out, "Location");
if (location) {
- start_foo = ap_strstr_c(location, master_uri);
+ remainder = ap_strstr_c(location, master_uri);
}
- if (start_foo) {
+ if (remainder) {
const char *new_uri;
- start_foo += strlen(master_uri);
+ remainder += strlen(master_uri);
+ /* REMAINDER is empty or begins with '/' (the stored master URI
+ is canonical, with no trailing slash), so concatenate
+ without a separator. */
new_uri = ap_construct_url(r->pool,
apr_pstrcat(r->pool,
- dav_svn__get_root_dir(r), "/",
- start_foo, SVN_VA_NULL),
+ dav_svn__get_root_dir(r),
+ remainder, SVN_VA_NULL),
r);
apr_table_set(r->headers_out, "Location", new_uri);
}
@@ -379,25 +402,9 @@ apr_status_t dav_svn__location_body_filt
return ap_pass_brigade(f->next, bb);
}
- /* ### FIXME (SVN-3445, residual): a PROPFIND multistatus is still
rewritten
- ### wholesale below, so a dead-property *value* that happens to contain
- ### the master location gets silently rewritten along with the genuine
- ### <D:href>s. Fixing that safely requires an XML-structure-aware
- ### rewrite (translate hrefs only, leave property values alone) rather
- ### than the blind byte substitution used here. */
-
- /* Both CANONICALIZED_URI and ROOT_DIR are already canonical and
- URI-encoded (svn_urlpath__canonicalize() output and the stored
- <Location> path, respectively), which is the same domain the
- protocol bodies use on the wire. */
if (!f->ctx) {
ctx = f->ctx = apr_pcalloc(r->pool, sizeof(*ctx));
- ctx->remotepath = canonicalized_uri;
- ctx->remotepath_len = strlen(ctx->remotepath);
- ctx->localpath = root_dir;
- ctx->localpath_len = strlen(ctx->localpath);
- ctx->pattern = apr_strmatch_precompile(r->pool, ctx->remotepath, 1);
- ctx->pattern_len = ctx->remotepath_len;
+ locate_ctx_init(ctx, r->pool, canonicalized_uri, root_dir);
}
bkt = APR_BRIGADE_FIRST(bb);
@@ -416,8 +423,8 @@ apr_status_t dav_svn__location_body_filt
apr_bucket_split(next_bucket, ctx->pattern_len);
bkt = APR_BUCKET_NEXT(next_bucket);
apr_bucket_delete(next_bucket);
- next_bucket = apr_bucket_pool_create(ctx->localpath,
- ctx->localpath_len,
+ next_bucket = apr_bucket_pool_create(ctx->replacement,
+ ctx->replacement_len,
r->pool, bb->bucket_alloc);
APR_BUCKET_INSERT_BEFORE(bkt, next_bucket);
}
Modified:
subversion/branches/proxy-move-copy-fix/subversion/tests/cmdline/dav-mirror-autocheck.sh
==============================================================================
---
subversion/branches/proxy-move-copy-fix/subversion/tests/cmdline/dav-mirror-autocheck.sh
Sat Aug 15 13:39:08 2026 (r1937141)
+++
subversion/branches/proxy-move-copy-fix/subversion/tests/cmdline/dav-mirror-autocheck.sh
Sat Aug 15 21:26:59 2026 (r1937142)
@@ -31,10 +31,13 @@
#
# The changes sent through the system started as the reproduction
# recipe for issue 2939 (https://issues.apache.org/jira/browse/SVN-2939,
-# using svnmucc) and have grown to cover URI-encoded locations, the
-# SVN-3445 payload-corruption regressions, proxied reads of transaction
-# resources, and locks. Any svn traffic liable to break over
-# mirroring remains a good addition.
+# using svnmucc) and have grown to cover URI-encoded locations,
+# COPY/MOVE Destination rewriting, the SVN-3445 payload-corruption
+# regressions, proxied reads of transaction resources (including
+# dead-property values and location-name collisions in rewritten
+# hrefs), HTTPv1 MKACTIVITY/CHECKOUT, revision-property changes
+# through the proxy, and locks. Any svn traffic liable to break
+# over mirroring remains a good addition.
#
# Most of the httpd setup was lifted from davautocheck.sh.
# The common boilerplate snippets to setup/start/stop httpd
@@ -64,6 +67,45 @@ function fail() {
stop_httpd_and_die
}
+# Authenticated curl against the test repositories. CURL is resolved
+# later; the functions below expand it at call time.
+function curl_auth() {
+ $CURL --silent --show-error --user jrandom:rayjandom "$@"
+}
+
+# Open a transaction on the master (raw HTTPv2 create-txn POST) and print
+# its SVN-Txn-Name. Used by probes that must read in-txn data through the
+# slave; committed reads are served locally and never hit the rewrite.
+function create_master_txn() {
+ curl_auth \
+ --request POST \
+ --header "Content-Type: application/vnd.svn-skel" --data "( create-txn )" \
+ --dump-header - --output /dev/null "$MASTER_URL/!svn/me" \
+ | sed -ne 's/^SVN-Txn-Name: *//p' | tr -d '\r'
+}
+
+function delete_master_txn() {
+ curl_auth --request DELETE --output /dev/null \
+ "$MASTER_URL/!svn/txn/$1" \
+ || say "WARNING: could not delete test txn $1 (continuing)" >&2
+}
+
+# PROPFIND PATH in a fresh master txn through the slave. Stores the
+# multistatus in DEST (a variable name) and leaves TXN_NAME set so
+# callers can assert on the txn id in hrefs. WHAT is a short label
+# for the fail message. Assigns in this shell so TXN_NAME survives
+# (command substitution would lose it).
+function propfind_slave_txr() {
+ local dest="$1"
+ local path="$2"
+ local what="$3"
+ TXN_NAME=$(create_master_txn)
+ [ -n "$TXN_NAME" ] || fail "could not create a txn on the master${what:+
($what)}"
+ printf -v "$dest" '%s' "$(curl_auth --request PROPFIND --header "Depth: 0" \
+ "$SLAVE_URL/!svn/txr/$TXN_NAME/$path")"
+ delete_master_txn "$TXN_NAME"
+}
+
function get_loadmodule_config() {
local SO="$($APXS -q LIBEXECDIR)/$1.so"
@@ -388,6 +430,12 @@ MASTER_URL="http://${MASTER_HOST}:${TEST
SLAVE_URL="http://${SLAVE_HOST}:${TEST_PORT}/${SLAVE_LOCATION_URI}"
SYNC_URL="http://${SLAVE_HOST}:${TEST_PORT}/${SYNC_LOCATION}"
+# User-data payloads that include the rewrite-anchor tag plus a location
+# root -- the byte sequence the body filter matches. Protocol XML
+# escapes this; file content and skels carry it raw.
+HREF_IN_MASTER="<D:href>/${MASTER_LOCATION_URI}"
+HREF_IN_SLAVE="<D:href>/${SLAVE_LOCATION_URI}"
+
BASE_URL="$SLAVE_URL"
# setup server and repositories
@@ -412,9 +460,11 @@ read SLAVE_UUID < "$SLAVE_REPOS/db/uuid"
[ "$SLAVE_UUID" = "$MASTER_UUID" ] \
|| fail "master/slave uuid mismatch"
# setup hooks:
-# slave allows revprop changes
+# slave and master allow revprop changes (the latter so a proxied
+# svn propset --revprop can succeed)
# master syncs changes to slave
echo "#!/bin/sh" > "$SLAVE_REPOS/hooks/pre-revprop-change"
+echo "#!/bin/sh" > "$MASTER_REPOS/hooks/pre-revprop-change"
echo "#!/bin/sh" > "$MASTER_REPOS/hooks/post-revprop-change"
echo "#!/bin/sh" > "$MASTER_REPOS/hooks/post-commit"
echo "$SVNSYNC --non-interactive sync '$SYNC_URL' --username=svnsync
--password=svnsync" \
@@ -423,6 +473,7 @@ echo "$SVNSYNC --non-interactive sync '$
>> "$MASTER_REPOS/hooks/post-commit"
chmod 0755 "$SLAVE_REPOS/hooks/pre-revprop-change"
+chmod 0755 "$MASTER_REPOS/hooks/pre-revprop-change"
chmod 0755 "$MASTER_REPOS/hooks/post-revprop-change"
chmod 0755 "$MASTER_REPOS/hooks/post-commit"
@@ -506,6 +557,21 @@ $SVNLOOK tree --full-paths "$MASTER_REPO
say "PASS: committing a path which has a space in it passes"
+# An explicit move is a COPY plus a DELETE of the source within one txn.
+# The Destination rewrite is the same path as the space-in-name COPY
+# above; this checks the pair lands as a move on the master.
+say "Test case for move (COPY + DELETE) through the proxy"
+
+$svnmucc mkdir "$BASE_URL/move-src" \
+ || fail "creating move source failed"
+$svnmucc mv "$BASE_URL/move-src" "$BASE_URL/move-src-moved" \
+ || fail "move through the proxy failed"
+$SVNLOOK tree --full-paths "$MASTER_REPOS" | grep -Fq "move-src-moved/" \
+ || fail "moved directory missing on the master"
+$SVNLOOK tree --full-paths "$MASTER_REPOS" | grep -Fq "move-src/" \
+ && fail "move source still present on the master"
+say "PASS: move (copy + delete) works through the proxy"
+
# Regression coverage for SVN-3445. When the master and slave locations
# differ, the proxy must translate the location prefix in *protocol* URLs
# (hrefs in MERGE/CHECKOUT bodies) but must NOT touch *user payload*:
@@ -513,30 +579,30 @@ say "PASS: committing a path which has a
# PROPPATCH bodies, and log-message/revprop skels in POST bodies.
say "Test case for versioned content munging (SVN-3445)"
-# File content must round-trip verbatim whether it embeds the master URL
-# or the slave URL (used to be mangled/rejected).
-echo "$MASTER_URL" > "$HTTPD_ROOT/master-url.txt"
-echo "$SLAVE_URL" > "$HTTPD_ROOT/slave-url.txt"
+# File content must round-trip verbatim whether it embeds the master or
+# slave rewrite-anchor sequence (used to be mangled/rejected).
+echo "$HREF_IN_MASTER" > "$HTTPD_ROOT/master-url.txt"
+echo "$HREF_IN_SLAVE" > "$HTTPD_ROOT/slave-url.txt"
$svnmucc put "$HTTPD_ROOT/master-url.txt" "$BASE_URL/master-url.txt" \
put "$HTTPD_ROOT/slave-url.txt" "$BASE_URL/slave-url.txt" \
- || fail "committing URL-bearing files failed (SVN-3445: PUT body rewritten?)"
+ || fail "committing href-bearing files failed (SVN-3445: PUT body
rewritten?)"
master_file_content=$($SVNLOOK cat "$SLAVE_REPOS" master-url.txt)
-[ "$master_file_content" = "$MASTER_URL" ] \
- || fail "file content embedding the master URL was munged: committed
'$MASTER_URL', slave stores '$master_file_content'"
+[ "$master_file_content" = "$HREF_IN_MASTER" ] \
+ || fail "file content embedding the master href tag was munged: committed
'$HREF_IN_MASTER', slave stores '$master_file_content'"
slave_file_content=$($SVNLOOK cat "$SLAVE_REPOS" slave-url.txt)
-[ "$slave_file_content" = "$SLAVE_URL" ] \
- || fail "file content embedding the slave URL was munged: committed
'$SLAVE_URL', slave stores '$slave_file_content'"
-say "PASS: file content is preserved verbatim regardless of embedded URL"
+[ "$slave_file_content" = "$HREF_IN_SLAVE" ] \
+ || fail "file content embedding the slave href tag was munged: committed
'$HREF_IN_SLAVE', slave stores '$slave_file_content'"
+say "PASS: file content is preserved verbatim regardless of embedded href tag"
# Property values must likewise round-trip verbatim: a PROPPATCH value that
-# contains the slave URL used to be silently rewritten.
-$svnmucc propset svn-3445-prop "$SLAVE_URL" "$BASE_URL/slave-url.txt" \
- || fail "propset of a value containing the slave URL failed"
+# contains the rewrite-anchor sequence used to be silently rewritten.
+$svnmucc propset svn-3445-prop "$HREF_IN_SLAVE" "$BASE_URL/slave-url.txt" \
+ || fail "propset of a value containing the slave href tag failed"
prop_value=$($SVNLOOK propget "$SLAVE_REPOS" svn-3445-prop slave-url.txt)
-[ "$prop_value" = "$SLAVE_URL" ] \
- || fail "property value embedding the slave URL was munged: set
'$SLAVE_URL', slave stores '$prop_value'"
-say "PASS: property value is preserved verbatim regardless of embedded URL"
+[ "$prop_value" = "$HREF_IN_SLAVE" ] \
+ || fail "property value embedding the slave href tag was munged: set
'$HREF_IN_SLAVE', slave stores '$prop_value'"
+say "PASS: property value is preserved verbatim regardless of embedded href
tag"
# Commit log messages and revision properties travel inside the
# create-txn-with-props POST body (HTTPv2), a length-prefixed skel
@@ -544,24 +610,35 @@ say "PASS: property value is preserved v
# corrupts the values, and when the location paths differ in length it
# breaks the skel framing outright. Both must round-trip verbatim.
# (The $svnmucc wrapper bakes in -mm, so invoke $SVNMUCC directly.)
-log_msg="log mentioning the slave URL: $SLAVE_URL"
+log_msg="log mentioning the slave href tag: $HREF_IN_SLAVE"
$SVNMUCC --non-interactive --username jrandom --password rayjandom \
- -m "$log_msg" --with-revprop "svn-3445-revprop=$SLAVE_URL" \
+ -m "$log_msg" --with-revprop "svn-3445-revprop=$HREF_IN_SLAVE" \
mkdir "$BASE_URL/log-url-dir" \
- || fail "commit with a log message containing the slave URL failed (POST
body rewritten?)"
+ || fail "commit with a log message containing the slave href tag failed
(POST body rewritten?)"
rev=$($SVNLOOK youngest "$SLAVE_REPOS")
stored_log=$($SVNLOOK propget --revprop -r "$rev" "$SLAVE_REPOS" svn:log)
[ "$stored_log" = "$log_msg" ] \
|| fail "log message was munged: committed '$log_msg', slave stores
'$stored_log'"
stored_revprop=$($SVNLOOK propget --revprop -r "$rev" "$SLAVE_REPOS"
svn-3445-revprop)
-[ "$stored_revprop" = "$SLAVE_URL" ] \
- || fail "revprop value was munged: set '$SLAVE_URL', slave stores
'$stored_revprop'"
+[ "$stored_revprop" = "$HREF_IN_SLAVE" ] \
+ || fail "revprop value was munged: set '$HREF_IN_SLAVE', slave stores
'$stored_revprop'"
say "PASS: log message and revprop values are preserved verbatim"
-# The response side (mirror.c attaches the body-rewrite filter only for
-# MERGE and PROPFIND, with the response_is_xml() gate as a backstop) is
-# covered by the proxied-txn-reads test below. The residual block further
-# down exercises the one remaining SVN-3445 residual.
+# PROPPATCH on !svn/rev is a different path from the create-txn-with-props
+# POST above: the body is an opaque property value and must stay out of
+# the rewrite. The master's pre-revprop-change hook is enabled above
+# so this can succeed.
+say "Test case for revision-property change through the proxy"
+
+rev=$($SVNLOOK youngest "$MASTER_REPOS")
+$SVN propset --revprop -r "$rev" --non-interactive \
+ --username jrandom --password rayjandom \
+ test:revprop "url is $HREF_IN_SLAVE" "$SLAVE_URL" \
+ || fail "revprop change through the proxy failed"
+stored_rp=$($SVNLOOK propget --revprop -r "$rev" "$MASTER_REPOS" test:revprop)
+[ "$stored_rp" = "url is $HREF_IN_SLAVE" ] \
+ || fail "revprop value was munged through the proxy: stored '$stored_rp'"
+say "PASS: revision property change through the proxy is stored verbatim"
# Response-side SVN-3445 coverage: reads of transaction resources are
# proxied to the master; the proxy must rewrite hrefs in protocol XML
@@ -575,101 +652,116 @@ say "PASS: log message and revprop value
# without ever traversing the proxy or the response filter under test.
say "Test case for proxied reads of txn resources (SVN-3445 response side)"
-curl_auth="$CURL --silent --show-error --user jrandom:rayjandom"
-
-# Commit an XML-mime-typed file for probe 3 below (before the txn is
+# Commit an XML-mime-typed file for the GET probe below (before the txn is
# opened, so the txn tree contains it).
-printf '<?xml version="1.0"?>\n<note><!-- %s --></note>\n' "$MASTER_URL" \
+printf '<?xml version="1.0"?>\n<note><!-- %s --></note>\n' "$HREF_IN_MASTER" \
> "$HTTPD_ROOT/xml-payload.xml"
$svnmucc put "$HTTPD_ROOT/xml-payload.xml" "$BASE_URL/xml-payload.xml" \
propset svn:mime-type text/xml "$BASE_URL/xml-payload.xml" \
|| fail "committing the XML-typed payload file failed"
-# Open a transaction directly on the master (raw HTTPv2 create-txn POST)
-# and harvest its name from the SVN-Txn-Name response header.
-txn_name=$($curl_auth --request POST \
- --header "Content-Type: application/vnd.svn-skel" --data "( create-txn )" \
- --dump-header - --output /dev/null "$MASTER_URL/!svn/me" \
- | sed -ne 's/^SVN-Txn-Name: *//p' | tr -d '\r')
+txn_name=$(create_master_txn)
[ -n "$txn_name" ] || fail "could not create a txn on the master"
-# 1. Non-XML payload: GET the file through the SLAVE. The response is
-# proxied from the master and must arrive verbatim, the slave URL
-# embedded in the content must NOT have been rewritten.
-proxied_get=$($curl_auth "$SLAVE_URL/!svn/txr/$txn_name/slave-url.txt")
-[ "$proxied_get" = "$SLAVE_URL" ] \
- || fail "proxied GET of txn file content was munged: expected '$SLAVE_URL',
got '$proxied_get'"
-
-# 2. Protocol XML: PROPFIND on the txn file through the SLAVE. The
-# multistatus hrefs come from the master and MUST be rewritten to the
-# slave location.
-proxied_propfind=$($curl_auth --request PROPFIND --header "Depth: 0" \
- "$SLAVE_URL/!svn/txr/$txn_name/slave-url.txt")
+# Non-XML payload: GET the file through the SLAVE. The response is
+# proxied from the master and must arrive verbatim, the slave href
+# tag embedded in the content must NOT have been rewritten.
+proxied_get=$(curl_auth "$SLAVE_URL/!svn/txr/$txn_name/slave-url.txt")
+[ "$proxied_get" = "$HREF_IN_SLAVE" ] \
+ || fail "proxied GET of txn file content was munged: expected
'$HREF_IN_SLAVE', got '$proxied_get'"
+
+# Regression guard: a versioned file whose svn:mime-type is XML must
+# never have its content rewritten on a proxied read. Doubly protected:
+# GET responses do not receive the ReposRewrite body filter at all
+# (proxy_request_fixup() attaches it only for MERGE and PROPFIND), and
+# mod_dav_svn happens to emit no Content-Type for !svn/txr file GETs.
+proxied_xml_get=$(curl_auth "$SLAVE_URL/!svn/txr/$txn_name/xml-payload.xml")
+echo "$proxied_xml_get" | grep -qF "$HREF_IN_MASTER" \
+ || fail "XML-typed txn file content did not round-trip a proxied read
verbatim: got '$proxied_xml_get'"
+delete_master_txn "$txn_name"
+say "PASS: XML-typed txn file content survives a proxied read verbatim"
+
+# Protocol XML: PROPFIND on the txn file through the SLAVE. The
+# multistatus hrefs come from the master and MUST be rewritten to the
+# slave location.
+propfind_slave_txr proxied_propfind "slave-url.txt" "proxied reads"
echo "$proxied_propfind" | grep -qF "/${SLAVE_LOCATION_URI}/" \
|| fail "proxied PROPFIND multistatus hrefs were not rewritten to the slave
location: $proxied_propfind"
echo "$proxied_propfind" | grep -qF "/${MASTER_LOCATION_URI}/" \
&& fail "proxied PROPFIND multistatus still contains master-location hrefs:
$proxied_propfind"
-# 3. Regression guard: a versioned file whose svn:mime-type is XML must
-# never have its content rewritten on a proxied read. Doubly protected:
-# GET responses do not receive the ReposRewrite body filter at all
-# (proxy_request_fixup() attaches it only for MERGE and PROPFIND), and
-# mod_dav_svn happens to emit no Content-Type for !svn/txr file GETs.
-proxied_xml_get=$($curl_auth "$SLAVE_URL/!svn/txr/$txn_name/xml-payload.xml")
-echo "$proxied_xml_get" | grep -qF "$MASTER_URL" \
- || fail "XML-typed txn file content did not round-trip a proxied read
verbatim: got '$proxied_xml_get'"
-say "PASS: XML-typed txn file content survives a proxied read verbatim"
-
-# Clean up the open txn so later consistency checks aren't confused.
-$curl_auth --request DELETE --output /dev/null \
- "$MASTER_URL/!svn/txn/$txn_name" \
- || say "WARNING: could not delete test txn $txn_name (continuing)"
-
say "PASS: proxied txn reads: content verbatim, protocol hrefs rewritten"
-# The one remaining SVN-3445 residual: a dead-property value rewritten in
-# a proxied PROPFIND multistatus. Needs a proxied read of in-transaction
-# data (reads of committed data are served locally by the slave and never
-# traverse the response filter), so it reuses the curl txn-probe technique
-# from the test above.
-say "Test case for the SVN-3445 response-side residual (XFAIL expected)"
-
-# The munged fingerprint: a master-URL value rewritten by the response
-# filter keeps the master host but gains the slave location path.
-master_url_munged="http://${MASTER_HOST}:${TEST_PORT}/${SLAVE_LOCATION_URI}"
-
-# The residual: a dead-property VALUE containing the master URL, returned
-# inside a proxied PROPFIND multistatus, is blindly rewritten along with
-# the genuine hrefs (see the "FIXME (SVN-3445, residual)" comment in
-# mirror.c's dav_svn__location_body_filter).
-$svnmucc propset svn-3445-residual-prop "$MASTER_URL"
"$BASE_URL/master-url.txt" \
- || fail "propset of a master-URL value failed"
-
-# Open a fresh txn on the master (after the propset above, so its tree
-# contains the property).
-txn_name=$($curl_auth --request POST \
- --header "Content-Type: application/vnd.svn-skel" --data "( create-txn )" \
- --dump-header - --output /dev/null "$MASTER_URL/!svn/me" \
- | sed -ne 's/^SVN-Txn-Name: *//p' | tr -d '\r')
-[ -n "$txn_name" ] || fail "could not create a txn on the master (residual
tests)"
-
-# Residual probe: PROPFIND (allprop) on the file through the SLAVE.
-residual_propfind=$($curl_auth --request PROPFIND --header "Depth: 0" \
- "$SLAVE_URL/!svn/txr/$txn_name/master-url.txt")
-if echo "$residual_propfind" | grep -qF "$MASTER_URL"; then
- say "XPASS: SVN-3445 residual appears fixed: dead-property value survived a"
- say " proxied PROPFIND verbatim."
-elif echo "$residual_propfind" | grep -qF "$master_url_munged"; then
- say "XFAIL (SVN-3445 residual): dead-property value was rewritten in the"
- say " proxied multistatus: '$MASTER_URL' -> '$master_url_munged'."
-else
- fail "residual PROPFIND contained neither the original nor the munged value:
$residual_propfind"
-fi
+# Dead-property values in a proxied PROPFIND must survive verbatim while
+# the surrounding hrefs are translated. Needs a proxied read of in-txn
+# data (committed reads are served locally and never hit the filter).
+say "Test case for dead-property values in a proxied multistatus"
+
+$svnmucc propset url-bearing-prop "$HREF_IN_MASTER" "$BASE_URL/master-url.txt"
\
+ || fail "propset of a master href-tag value failed"
+
+propfind_slave_txr deadprop_propfind "master-url.txt" "dead-property test"
+# The value is XML-escaped in the multistatus; the raw tag must not appear
+# as a rewritten protocol href.
+echo "$deadprop_propfind" | grep -qF "<D:href>/${MASTER_LOCATION_URI}" \
+ || fail "dead-property value did not survive the proxied PROPFIND verbatim:
$deadprop_propfind"
+echo "$deadprop_propfind" | grep -qF "<D:href>/${SLAVE_LOCATION_URI}/" \
+ || fail "multistatus href was not translated to the slave location:
$deadprop_propfind"
+say "PASS: dead-property values survive proxied multistatus href translation"
+
+# A repos path component named like the master location must survive
+# root translation in proxied multistatus hrefs.
+say "Test case for location-name collision in proxied hrefs"
+
+$svnmucc mkdir "$BASE_URL/${MASTER_LOCATION_URI}" \
+ || fail "committing a directory named after the master location failed"
+$SVNLOOK tree --full-paths "$MASTER_REPOS" | grep -Fq "${MASTER_LOCATION}/" \
+ || fail "directory named after the master location missing on the master"
+
+propfind_slave_txr collision_propfind "${MASTER_LOCATION_URI}" "collision test"
+echo "$collision_propfind" | grep -qF "/$TXN_NAME/${MASTER_LOCATION_URI}" \
+ || fail "href component named after the master location did not survive the
proxied PROPFIND: $collision_propfind"
+echo "$collision_propfind" | grep -qF "<D:href>/${SLAVE_LOCATION_URI}/" \
+ || fail "collision multistatus href root was not translated to the slave
location: $collision_propfind"
+say "PASS: href components named after a location survive the anchored rewrite"
+
+# The HTTPv1 commit opening, emulated with curl (no modern client speaks
+# it, but mod_dav_svn still serves it): MKACTIVITY names an activity, and
+# CHECKOUT of a version resource carries that activity's href -- as
+# <D:href> in the request body -- which the request-side filter must
+# translate for the master to resolve it. The 201 itself proves the body
+# translation (an untranslated activity href cannot resolve on the
+# master); the Location header must come back rewritten to the slave
+# root with no doubled slash; and a PROPFIND of the resulting working
+# resource covers the !svn/wrk/ routing branch.
+say "Test case for v1 commit opening (MKACTIVITY/CHECKOUT) through the proxy"
+
+v1_activity="dav-mirror-v1-activity-$$"
+mka_status=$(curl_auth --request MKACTIVITY -o /dev/null -w "%{http_code}" \
+ "$SLAVE_URL/!svn/act/$v1_activity")
+[ "$mka_status" = "201" ] \
+ || fail "MKACTIVITY through the proxy failed (HTTP $mka_status)"
-# Clean up the open txn.
-$curl_auth --request DELETE --output /dev/null \
- "$MASTER_URL/!svn/txn/$txn_name" \
- || say "WARNING: could not delete residual-test txn $txn_name (continuing)"
+rev=$($SVNLOOK youngest "$SLAVE_REPOS")
+printf '<?xml version="1.0" encoding="utf-8"?><D:checkout
xmlns:D="DAV:"><D:activity-set><D:href>/%s/!svn/act/%s</D:href></D:activity-set></D:checkout>'
\
+ "$SLAVE_LOCATION_URI" "$v1_activity" > "$HTTPD_ROOT/checkout-body.xml"
+v1_location=$(curl_auth --request CHECKOUT --header "Content-Type: text/xml" \
+ --data @"$HTTPD_ROOT/checkout-body.xml" --dump-header - --output /dev/null \
+ "$SLAVE_URL/!svn/ver/$rev/master-url.txt" \
+ | sed -ne 's/^Location: *//p' | tr -d '\r')
+[ -n "$v1_location" ] \
+ || fail "v1 CHECKOUT through the proxy returned no Location header (body
href untranslated?)"
+echo "$v1_location" | grep -qF
"$SLAVE_URL/!svn/wrk/$v1_activity/master-url.txt" \
+ || fail "CHECKOUT Location was not rewritten cleanly to the slave root:
'$v1_location'"
+
+wrk_propfind=$(curl_auth --request PROPFIND --header "Depth: 0" \
+ "$SLAVE_URL/!svn/wrk/$v1_activity/master-url.txt")
+echo "$wrk_propfind" | grep -qF "<D:href>/${SLAVE_LOCATION_URI}/!svn/wrk/" \
+ || fail "working-resource PROPFIND href was not translated to the slave
location: $wrk_propfind"
+
+curl_auth --request DELETE --output /dev/null
"$SLAVE_URL/!svn/act/$v1_activity" \
+ || say "WARNING: could not delete v1 test activity (continuing)" >&2
+say "PASS: v1 activity checkout, Location rewrite, and wrk reads work through
the proxy"
# LOCK/UNLOCK are proxied methods, and locked-file commits push lock tokens
# through the proxy. The lock comment travels as the DAV owner element in
@@ -679,7 +771,7 @@ say "Test case for locks through the wri
svncmd="$SVN --non-interactive --username=jrandom --password=rayjandom"
$svncmd checkout -q "$BASE_URL" "$HTTPD_ROOT/wc-lock" \
|| fail "checkout for lock test failed"
-$svncmd lock -m "locked via slave: $SLAVE_URL"
"$HTTPD_ROOT/wc-lock/slave-url.txt" \
+$svncmd lock -m "locked via slave: $HREF_IN_SLAVE"
"$HTTPD_ROOT/wc-lock/slave-url.txt" \
|| fail "svn lock through the proxy failed"
# The lock must exist on the MASTER (locks are not versioned; svnsync does
@@ -690,8 +782,8 @@ $SVNLOOK lock "$MASTER_REPOS" slave-url.
# The client's own view of the comment is just the cached copy in its
# working-copy lock table, so inspect what the master actually stored.
lock_comment=$($SVNLOOK lock "$MASTER_REPOS" slave-url.txt)
-echo "$lock_comment" | grep -qF "$SLAVE_URL" \
- || fail "stored lock comment was munged by the proxy: expected it to contain
'$SLAVE_URL', master stores: $lock_comment"
+echo "$lock_comment" | grep -qF "$HREF_IN_SLAVE" \
+ || fail "stored lock comment was munged by the proxy: expected it to contain
'$HREF_IN_SLAVE', master stores: $lock_comment"
say "PASS: lock comment survives the proxy verbatim"
# Committing a change to the locked file sends the lock token with the