Compare commits

...

4 Commits

Author SHA1 Message Date
Xavier Roche
f7d45027a8 Merge branch 'master' into fix/proxytrack-dav-default-doc
Union both sides' expected-skip entries and TESTS-tail additions,
deduplicating and restoring numeric order.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 17:32:15 +02:00
Xavier Roche
13953867e8 Merge remote-tracking branch 'origin/master' into fix/proxytrack-dav-default-doc
Signed-off-by: Xavier Roche <roche@httrack.com>

# Conflicts:
#	tests/Makefile.am
2026-07-27 15:27:58 +02:00
Xavier Roche
db89c82d79 Pin the new skip on Windows and tighten the listing assertions
The Windows job runs *_local-*.test and compares the skip list against an
exact string, so an unpinned skip fails the leg with fail=0, which reads
like a flake. Assert the href and the response count too: displayname
alone comes from the href's trailing component, so it cannot see a wrong
path above it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 15:11:55 +02:00
Xavier Roche
ad3f051ac6 A PROPFIND on an exact cache entry crashes proxytrack
PT_Enumerate() reports a folder's default document as a zero-length name,
and the WebDAV listing loop read thisUrl[thisUrlLen - 1] on it, four
gigabytes past the string. One unauthenticated request took the whole
listener down.

Closes #828

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 14:40:41 +02:00
4 changed files with 128 additions and 6 deletions

View File

@@ -260,11 +260,11 @@ jobs:
# tested nothing: pin the skips, and floor the passes in case the glob empties.
# footer-overflow and purge-longpath skip on Windows (need a path past MAX_PATH);
# crange pending #581;
# webdav-mime needs a reapable background listener, which MSYS cannot give it;
# webdav-default and webdav-mime need a reapable background listener, which MSYS cannot give them;
# badmtime needs a filesystem that stores an mtime past gmtime's range;
# single-file ends on a GUI half needing htsserver, which this job does not build;
# update-304-leak needs a LeakSanitizer build, which MSVC has no equivalent of.
expected_skips=" 01_engine-footer-overflow.test 100_local-purge-longpath.test 114_local-update-304-leak.test 48_local-crange-memresume.test 71_local-crange-repaircache.test 79_local-proxytrack-webdav-mime.test 88_local-proxytrack-badmtime.test 94_local-single-file.test"
expected_skips=" 01_engine-footer-overflow.test 100_local-purge-longpath.test 114_local-update-304-leak.test 120_local-proxytrack-webdav-default.test 48_local-crange-memresume.test 71_local-crange-repaircache.test 79_local-proxytrack-webdav-mime.test 88_local-proxytrack-badmtime.test 94_local-single-file.test"
# First, or the deadline reads as an unexplained shortfall in the gates below.
[ "$deadline" -eq 0 ] || { echo "::error::suite did not finish within ${suite_deadline}s"; exit 1; }
[ "$pass" -ge 90 ] || { echo "::error::only $pass tests passed ($skip skipped)"; exit 1; }

View File

@@ -748,7 +748,8 @@ static PT_Element proxytrack_process_DAV_Request(PT_Indexes indexes,
const char *thisUrl = list[i];
const char *mimeType = "application/octet-stream";
unsigned int thisUrlLen = (unsigned int) strlen(thisUrl);
int thisIsDir = (thisUrl[thisUrlLen - 1] == '/') ? 1 : 0;
/* the folder's default document is enumerated as an empty name */
int thisIsDir = (hts_lastchar(thisUrl) == '/') ? 1 : 0;
/* Item URL */
StringRoom(itemUrl,
@@ -853,8 +854,7 @@ static PT_Element proxytrack_process_HTTP_List(PT_Indexes indexes,
for(isDir = 1; isDir >= 0; isDir--) {
for(i = 0; list[i] != NULL; i++) {
char *thisUrl = list[i];
unsigned int thisUrlLen = (unsigned int) strlen(thisUrl);
int thisIsDir = (thisUrl[thisUrlLen - 1] == '/') ? 1 : 0;
int thisIsDir = (hts_lastchar(thisUrl) == '/') ? 1 : 0;
if (thisIsDir == isDir) {
if (isDir)

View File

@@ -0,0 +1,121 @@
#!/bin/bash
#
# A PROPFIND whose path is an exact cache entry enumerates that entry's default
# document under an empty name; proxytrack used to index one byte before it and
# die on the whole listener.
set -euo pipefail
: "${top_srcdir:=..}"
# shellcheck source=tests/testlib.sh
. "$top_srcdir/tests/testlib.sh"
python=$(find_python) || {
echo "python3 missing, skipping"
exit 77
}
command -v curl >/dev/null 2>&1 || {
echo "curl missing, skipping"
exit 77
}
# MSYS cannot reap a native listener, and the orphan wedges the suite (#595).
if is_windows; then
echo "windows: cannot reap a backgrounded proxytrack, skipping"
exit 77
fi
dir=$(mktemp -d)
ptpid=
cleanup() {
stop_server "$ptpid"
rm -rf "$dir"
}
trap 'set +e; cleanup' EXIT
trap 'set +e; cleanup; exit 1' INT TERM HUP
printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 5\r\n\r\n' >"$dir/hdr"
printf 'hello' >"$dir/body"
alen=$(($(wc -c <"$dir/hdr") + $(wc -c <"$dir/body")))
{
printf 'filedesc://t.arc 0.0.0.0 20250101000000 text/plain 200 - - 0 t.arc 9\n'
printf '2 0 test\n'
printf '\n\n'
printf 'http://example.com/page.html 0.0.0.0 20250101000000 text/html 200 - - 0 t.arc %d\n' "$alen"
cat "$dir/hdr" "$dir/body"
} >"$dir/in.arc"
freeport() {
"$python" -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()'
}
proxyport=$(freeport)
icpport=$(freeport)
proxytrack "127.0.0.1:$proxyport" "127.0.0.1:$icpport" "$dir/in.arc" >"$dir/pt.log" 2>&1 &
ptpid=$!
waited=0
until grep -qE "HTTP Proxy installed on|Unable to (initialize a temporary server|create the server)" "$dir/pt.log"; do
kill -0 "$ptpid" 2>/dev/null || {
echo "FAIL: proxytrack exited before listening"
cat "$dir/pt.log"
exit 1
}
test "$waited" -lt 50 || {
echo "FAIL: proxytrack never announced its listen port"
exit 1
}
sleep 0.1
waited=$((waited + 1))
done
grep -q "HTTP Proxy installed on" "$dir/pt.log" || {
echo "FAIL: proxytrack failed to bind"
cat "$dir/pt.log"
exit 1
}
propfind() {
curl -s -i --max-time 30 -X PROPFIND -H "Depth: 1" \
"http://127.0.0.1:$proxyport/webdav/example.com/$1"
}
resp=$(propfind page.html) || {
echo "FAIL: PROPFIND on an exact cache entry did not answer"
cat "$dir/pt.log"
exit 1
}
grep -q '^HTTP/1\.[01] 207 ' <<<"$resp" || {
echo "FAIL: expected 207 Multi-Status"
printf '%s\n' "$resp"
exit 1
}
grep -q '<displayname>Default Document for the Folder</displayname>' <<<"$resp" || {
echo "FAIL: the default document of the entry was not listed"
printf '%s\n' "$resp"
exit 1
}
# The name alone would still pass on a truncated or garbage URL, so pin the
# href: the default document is the collection path plus an empty name.
grep -q '<href>/webdav/example\.com/page\.html/</href>' <<<"$resp" || {
echo "FAIL: the default document was listed under the wrong href"
printf '%s\n' "$resp"
exit 1
}
responses=$(grep -c '<response ' <<<"$resp" || true)
test "$responses" -eq 2 || {
echo "FAIL: expected the root and its default document, got $responses responses"
printf '%s\n' "$resp"
exit 1
}
# The crash killed the process, so a second request is the liveness proof.
resp=$(propfind "") || {
echo "FAIL: proxytrack died serving the previous PROPFIND"
cat "$dir/pt.log"
exit 1
}
grep -q '^HTTP/1\.[01] 207 ' <<<"$resp" || {
echo "FAIL: expected 207 Multi-Status on the follow-up listing"
printf '%s\n' "$resp"
exit 1
}
echo "OK: PROPFIND on an exact cache entry lists its default document and survives"

View File

@@ -230,6 +230,7 @@ TESTS = \
105_suite-timeout.test \
110_local-ftp-parallel.test \
111_local-ftp-update-rest.test \
114_local-update-304-leak.test
114_local-update-304-leak.test \
120_local-proxytrack-webdav-default.test
CLEANFILES = check-network_sh.cache