mirror of
https://github.com/xroche/httrack.git
synced 2026-07-12 20:07:43 +03:00
Compare commits
2 Commits
fix-codeql
...
fix-c7-304
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e04bc4be3 | ||
|
|
a40265fab6 |
@@ -3512,6 +3512,11 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
|
||||
continue;
|
||||
}
|
||||
|
||||
// The server really sent 304 here; the *-hacks below force
|
||||
// NOT_MODIFIED only after confirming the file is complete.
|
||||
const hts_boolean server_sent_304 =
|
||||
(back[i].r.statuscode == HTTP_NOT_MODIFIED);
|
||||
|
||||
/*
|
||||
Solve "false" 416 problems
|
||||
*/
|
||||
@@ -3701,6 +3706,21 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
|
||||
}
|
||||
}
|
||||
|
||||
// Out-of-protocol 304 to a Range resume: the file is still
|
||||
// partial, so drop it and refetch instead of trusting it.
|
||||
if (server_sent_304 && back[i].range_req_size > 0) {
|
||||
url_savename_refname_remove(opt, back[i].url_adr,
|
||||
back[i].url_fil);
|
||||
UNLINK(back[i].url_sav);
|
||||
deletehttp(&back[i].r);
|
||||
back[i].r.soc = INVALID_SOCKET;
|
||||
back[i].r.statuscode = STATUSCODE_NON_FATAL;
|
||||
strcpybuff(back[i].r.msg,
|
||||
"Bogus 304 on resume, restarting");
|
||||
back[i].status = STATUS_READY;
|
||||
back_set_finished(sback, i);
|
||||
}
|
||||
|
||||
/* sinon, continuer */
|
||||
/* if (back[i].r.soc!=INVALID_SOCKET) { // ok récupérer body? */
|
||||
// head: terminé
|
||||
|
||||
118
tests/46_local-update-304-resume.test
Executable file
118
tests/46_local-update-304-resume.test
Executable file
@@ -0,0 +1,118 @@
|
||||
#!/bin/bash
|
||||
# C7: a server 304 answering a resume's Range request is out of protocol; httrack
|
||||
# must drop the partial and refetch, not finalize it as complete at the partial
|
||||
# byte count. Pass 1 leaves a partial + temp-ref; pass 2 resumes, gets a bogus
|
||||
# 304, and must recover the whole file.
|
||||
set -u
|
||||
|
||||
: "${top_srcdir:=..}"
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
server="${testdir}/local-server.py"
|
||||
|
||||
command -v python3 >/dev/null || ! echo "python3 not found; skipping" || exit 77
|
||||
|
||||
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_c7.XXXXXX") || exit 1
|
||||
serverpid=
|
||||
crawlpid=
|
||||
cleanup() {
|
||||
test -n "$crawlpid" && kill -9 "$crawlpid" 2>/dev/null
|
||||
if test -n "$serverpid"; then
|
||||
kill "$serverpid" 2>/dev/null
|
||||
wait "$serverpid" 2>/dev/null
|
||||
fi
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
trap cleanup EXIT HUP INT QUIT PIPE TERM
|
||||
|
||||
serverlog="${tmpdir}/server.log"
|
||||
counter="${tmpdir}/reqcount"
|
||||
mark="${tmpdir}/got304"
|
||||
RESUME304_COUNTER="$counter" RESUME304_MARK="$mark" python3 "$server" --root "${testdir}/server-root" >"$serverlog" 2>&1 &
|
||||
serverpid=$!
|
||||
port=
|
||||
for _ in $(seq 1 50); do
|
||||
line=$(head -n1 "$serverlog" 2>/dev/null)
|
||||
if test "${line%% *}" == "PORT"; then
|
||||
port="${line#PORT }"
|
||||
break
|
||||
fi
|
||||
kill -0 "$serverpid" 2>/dev/null || {
|
||||
echo "server exited early: $(cat "$serverlog")"
|
||||
exit 1
|
||||
}
|
||||
sleep 0.1
|
||||
done
|
||||
test -n "$port" || {
|
||||
echo "could not discover server port"
|
||||
exit 1
|
||||
}
|
||||
base="http://127.0.0.1:${port}"
|
||||
|
||||
which httrack >/dev/null || {
|
||||
echo "could not find httrack"
|
||||
exit 1
|
||||
}
|
||||
out="${tmpdir}/crawl"
|
||||
mkdir "$out"
|
||||
common=(-O "$out" --quiet --disable-security-limits --robots=0 --timeout=30 --retries=1 -c1)
|
||||
refdir="${out}/hts-cache/ref"
|
||||
|
||||
# --- pass 1: crawl, interrupt once the blob download is underway -------------
|
||||
printf '[pass 1: interrupt mid-download] ..\t'
|
||||
httrack "${common[@]}" "${base}/resume304/index.html" >"${tmpdir}/log1" 2>&1 &
|
||||
crawlpid=$!
|
||||
for _ in $(seq 1 300); do
|
||||
test -s "$counter" && break
|
||||
kill -0 "$crawlpid" 2>/dev/null || break
|
||||
sleep 0.1
|
||||
done
|
||||
sleep 0.5
|
||||
kill -TERM "$crawlpid" 2>/dev/null
|
||||
wait "$crawlpid" 2>/dev/null
|
||||
crawlpid=
|
||||
test -n "$(find "$refdir" -name '*.ref' 2>/dev/null)" || {
|
||||
echo "FAIL: no temp-ref survived pass 1; cannot drive C7"
|
||||
exit 1
|
||||
}
|
||||
echo "OK (temp-ref present)"
|
||||
before=$(wc -c <"$counter" 2>/dev/null || echo 0)
|
||||
|
||||
# --- pass 2: --continue -> resume Range -> bogus 304 -> drop + refetch --------
|
||||
printf '[pass 2: resume gets 304, must refetch] ..\t'
|
||||
httrack "${common[@]}" --continue "${base}/resume304/index.html" >"${tmpdir}/log2" 2>&1
|
||||
echo "OK (terminated)"
|
||||
|
||||
blob=$(find "$out" -name blob.bin 2>/dev/null | head -1)
|
||||
full=8200 # len(RESUME304_BODY) = len("C7DATA--") + 8192
|
||||
|
||||
printf '[file recovered whole] ..\t'
|
||||
test -s "$blob" || {
|
||||
echo "FAIL: blob.bin missing after the 304 resume (partial dropped, not refetched)"
|
||||
exit 1
|
||||
}
|
||||
got=$(wc -c <"$blob")
|
||||
test "$got" -eq "$full" || {
|
||||
echo "FAIL: blob.bin is ${got} bytes, expected ${full} (partial finalized as complete)"
|
||||
exit 1
|
||||
}
|
||||
echo "OK (${got} bytes)"
|
||||
|
||||
# Pass 2 must issue at least two requests: the resume (Range -> 304) and the
|
||||
# range-less refetch (-> 200). Exactly one means the partial was trusted.
|
||||
after=$(wc -c <"$counter" 2>/dev/null || echo 0)
|
||||
hits=$((after - before))
|
||||
printf '[resume then refetch] ..\t'
|
||||
test "$hits" -ge 2 || {
|
||||
echo "FAIL: only ${hits} pass-2 request(s); the 304 was trusted, no refetch"
|
||||
exit 1
|
||||
}
|
||||
echo "OK (${hits} requests)"
|
||||
|
||||
# The recovery must go through the resume path: a Range request that drew the
|
||||
# bogus 304 (a fresh re-crawl sends no Range, so this marker stays empty).
|
||||
printf '[resume Range drew a 304] ..\t'
|
||||
test -s "$mark" || {
|
||||
echo "FAIL: no Range request got a 304; the resume path was not exercised"
|
||||
exit 1
|
||||
}
|
||||
echo "OK"
|
||||
@@ -114,6 +114,7 @@ TESTS = \
|
||||
42_local-maxsize-slow.test \
|
||||
43_local-update-truncate.test \
|
||||
44_local-update-errormask.test \
|
||||
45_local-maxsize-recv.test
|
||||
45_local-maxsize-recv.test \
|
||||
46_local-update-304-resume.test
|
||||
|
||||
CLEANFILES = check-network_sh.cache
|
||||
|
||||
@@ -790,6 +790,56 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
|
||||
# C7: stall the first GET (partial + temp-ref), then answer the resume's
|
||||
# Range with a bogus 304; httrack must drop the partial and refetch.
|
||||
RESUME304_BODY = b"C7DATA--" + bytes((i * 7 + 3) % 256 for i in range(8192))
|
||||
_resume304_started = False
|
||||
|
||||
def route_resume304_index(self):
|
||||
self.send_html('\t<a href="blob.bin">blob</a>')
|
||||
|
||||
def route_resume304(self):
|
||||
counter = os.environ.get("RESUME304_COUNTER")
|
||||
if counter:
|
||||
with open(counter, "a") as fp:
|
||||
fp.write("x")
|
||||
rng = self.headers.get("Range")
|
||||
if not Handler._resume304_started:
|
||||
Handler._resume304_started = True
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/octet-stream")
|
||||
self.send_header("Content-Length", str(len(self.RESUME304_BODY)))
|
||||
self.send_header("Accept-Ranges", "bytes")
|
||||
self.send_header("Last-Modified", BIG_LASTMOD)
|
||||
self.send_header("ETag", '"c7"')
|
||||
self.end_headers()
|
||||
if self.command != "HEAD":
|
||||
self.wfile.write(self.RESUME304_BODY[:4096])
|
||||
self.wfile.flush()
|
||||
try:
|
||||
while True:
|
||||
time.sleep(3600)
|
||||
except OSError:
|
||||
pass
|
||||
return
|
||||
if rng is not None: # resume request: bogus out-of-protocol 304
|
||||
mark = os.environ.get("RESUME304_MARK")
|
||||
if mark:
|
||||
with open(mark, "a") as fp:
|
||||
fp.write("z")
|
||||
self.send_response(304)
|
||||
self.send_header("ETag", '"c7"')
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
return
|
||||
# Range-less refetch after the partial is dropped: whole file.
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/octet-stream")
|
||||
self.send_header("Content-Length", str(len(self.RESUME304_BODY)))
|
||||
self.end_headers()
|
||||
if self.command != "HEAD":
|
||||
self.wfile.write(self.RESUME304_BODY)
|
||||
|
||||
# 206 resume must honor the server's Content-Range, not the offset we asked
|
||||
# for (#198): a server resuming a few bytes *before* the request must not
|
||||
# leave httrack duplicating the overlap onto the partial. flaky.bin
|
||||
@@ -1149,6 +1199,8 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
"/intl/" + INTL_NAME: route_intl_page,
|
||||
"/resume/index.html": route_resume_index,
|
||||
"/resume/blob.txt": route_resume,
|
||||
"/resume304/index.html": route_resume304_index,
|
||||
"/resume304/blob.bin": route_resume304,
|
||||
"/overlap/index.html": route_overlap_index,
|
||||
"/overlap/flaky.bin": route_overlap,
|
||||
"/overlap/full.bin": route_overlap_full,
|
||||
|
||||
Reference in New Issue
Block a user