mirror of
https://github.com/xroche/httrack.git
synced 2026-07-12 20:07:43 +03:00
Compare commits
21 Commits
3.49.12
...
fix-test19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b571ac8616 | ||
|
|
b5f28f6eb5 | ||
|
|
3d65bb2e3b | ||
|
|
68897b09ce | ||
|
|
e8d1bf9c19 | ||
|
|
ef40277c24 | ||
|
|
797acecfd2 | ||
|
|
8237c3ed2b | ||
|
|
75b815e1fb | ||
|
|
8d02e60907 | ||
|
|
e70607e70c | ||
|
|
cada6a1ae3 | ||
|
|
145ea4769f | ||
|
|
c5da5a2f2e | ||
|
|
6eaefe6233 | ||
|
|
33249be5a0 | ||
|
|
167ede32c0 | ||
|
|
c8b7c63c9b | ||
|
|
04f37b2094 | ||
|
|
c07b19bc3d | ||
|
|
04bbb489cf |
34
.github/workflows/ci.yml
vendored
34
.github/workflows/ci.yml
vendored
@@ -55,7 +55,9 @@ jobs:
|
||||
run: make -j"$(nproc)"
|
||||
|
||||
- name: Test
|
||||
run: make check
|
||||
run: |
|
||||
jobs=$(( $(nproc) * 2 )); [ "$jobs" -le 16 ] || jobs=16
|
||||
make check -j"$jobs"
|
||||
|
||||
- name: Print the test log on failure
|
||||
if: failure()
|
||||
@@ -99,7 +101,8 @@ jobs:
|
||||
sudo find /usr/bin /usr/local/bin -maxdepth 1 -name 'python3*' \
|
||||
-exec mv {} {}.hidden \;
|
||||
! command -v python3
|
||||
make check
|
||||
jobs=$(( $(nproc) * 2 )); [ "$jobs" -le 16 ] || jobs=16
|
||||
make check -j"$jobs"
|
||||
|
||||
- name: Print the test log on failure
|
||||
if: failure()
|
||||
@@ -132,8 +135,18 @@ jobs:
|
||||
- name: Build
|
||||
run: make -j"$(sysctl -n hw.ncpu)"
|
||||
|
||||
- name: Add loopback aliases (macOS lacks 127.0.0.2/.3)
|
||||
# 19_local-connect-fallback needs the dead 127.0.0.2/.3 to refuse
|
||||
# instantly like Linux; alias them onto lo0 so they don't stall to timeout.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo ifconfig lo0 alias 127.0.0.2 up
|
||||
sudo ifconfig lo0 alias 127.0.0.3 up
|
||||
|
||||
- name: Test
|
||||
run: make check
|
||||
run: |
|
||||
jobs=$(( $(sysctl -n hw.ncpu) * 2 )); [ "$jobs" -le 16 ] || jobs=16
|
||||
make check -j"$jobs"
|
||||
|
||||
- name: Print the test log on failure
|
||||
if: failure()
|
||||
@@ -171,7 +184,9 @@ jobs:
|
||||
run: make -j"$(nproc)"
|
||||
|
||||
- name: Test
|
||||
run: make check
|
||||
run: |
|
||||
jobs=$(( $(nproc) * 2 )); [ "$jobs" -le 16 ] || jobs=16
|
||||
make check -j"$jobs"
|
||||
|
||||
- name: Print the test log on failure
|
||||
if: failure()
|
||||
@@ -226,7 +241,9 @@ jobs:
|
||||
env:
|
||||
ASAN_OPTIONS: detect_leaks=0:abort_on_error=1:halt_on_error=1:strict_string_checks=1:malloc_fill_byte=202:max_malloc_fill_size=2147483647:free_fill_byte=203:max_free_fill_size=2147483647
|
||||
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
|
||||
run: make check
|
||||
run: |
|
||||
jobs=$(( $(nproc) * 2 )); [ "$jobs" -le 16 ] || jobs=16
|
||||
make check -j"$jobs"
|
||||
|
||||
- name: Print the test log on failure
|
||||
if: failure()
|
||||
@@ -272,7 +289,8 @@ jobs:
|
||||
# 01_engine-* only; zlib-dependent self-tests are named 01_zlib-* and
|
||||
# skipped here (uninstrumented libz floods MSan with false positives).
|
||||
tests="$(cd tests && ls 01_engine-*.test | tr '\n' ' ')"
|
||||
make check TESTS="$tests"
|
||||
jobs=$(( $(nproc) * 2 )); [ "$jobs" -le 16 ] || jobs=16
|
||||
make check -j"$jobs" TESTS="$tests"
|
||||
|
||||
- name: Print the test log on failure
|
||||
if: failure()
|
||||
@@ -347,7 +365,9 @@ jobs:
|
||||
run: make -j"$(nproc)"
|
||||
|
||||
- name: Test
|
||||
run: make check
|
||||
run: |
|
||||
jobs=$(( $(nproc) * 2 )); [ "$jobs" -le 16 ] || jobs=16
|
||||
make check -j"$jobs"
|
||||
|
||||
- name: Print the test log on failure
|
||||
if: failure()
|
||||
|
||||
15
AGENTS.md
15
AGENTS.md
@@ -6,8 +6,19 @@ the operational checklist: toolchain, invariants, and how to ship a change.
|
||||
## Build & test
|
||||
- Fresh clone first: `git submodule update --init src/coucal`
|
||||
- `./bootstrap` (regenerates `configure` via `autoreconf`; needs autoconf,
|
||||
automake, libtool), then `bash configure && make && make check`. Or run
|
||||
`sh build.sh` to do bootstrap + configure + make in one shot.
|
||||
automake, libtool), then `bash configure && make -j"$(nproc)" && make check
|
||||
-j"$(nproc)"`. Always pass `-j` to `make check`: the suite runs under
|
||||
automake's parallel harness and each crawl test binds its own ephemeral-port
|
||||
server, so `-j` never contends and a multi-minute serial run drops to
|
||||
seconds. A new `.test` added to `$(TESTS)` is scheduled onto a free worker
|
||||
automatically; only a test slower than the current longest raises the floor.
|
||||
On a few-core Linux box, `-j` at 2x the core count is faster still: the tests
|
||||
spend much of their wall time asleep (server trickles, httrack self-pacing),
|
||||
so an idle core covers a sleeping one. CI uses `min(2*cores, 16)` on every
|
||||
platform, macOS included: the test server raises its listen backlog
|
||||
(`request_queue_size`) so macOS/BSD don't drop connections under a parallel
|
||||
`-c16` bigcrawl the way Python's default backlog of 5 did.
|
||||
Or run `sh build.sh` to do bootstrap + configure + make in one shot.
|
||||
|
||||
## Hard invariants
|
||||
- **Generated autotools files are NOT in git.** `configure`, every
|
||||
|
||||
270
src/htsback.c
270
src/htsback.c
@@ -68,6 +68,15 @@ static int slot_can_be_cached_on_disk(const lien_back * back);
|
||||
static int slot_can_be_cleaned(const lien_back * back);
|
||||
static int slot_can_be_finalized(httrackp * opt, const lien_back * back);
|
||||
|
||||
/* Which hard quota, if any, is currently aborting the mirror. */
|
||||
typedef enum {
|
||||
HTS_MIRROR_LIMIT_NONE = 0,
|
||||
HTS_MIRROR_LIMIT_SIZE,
|
||||
HTS_MIRROR_LIMIT_TIME,
|
||||
} hts_mirror_limit;
|
||||
|
||||
static hts_mirror_limit back_mirror_limit(httrackp *opt);
|
||||
|
||||
struct_back *back_new(httrackp *opt, int back_max) {
|
||||
int i;
|
||||
struct_back *sback = calloct(1, sizeof(struct_back));
|
||||
@@ -524,22 +533,22 @@ int back_nsoc_overall(const struct_back * sback) {
|
||||
|
||||
/* generate temporary file on lien_back */
|
||||
/* Note: utf-8 */
|
||||
static int create_back_tmpfile(httrackp * opt, lien_back *const back) {
|
||||
static int create_back_tmpfile(httrackp *opt, lien_back *const back,
|
||||
const char *ext) {
|
||||
// do not use tempnam() but a regular filename
|
||||
back->tmpfile_buffer[0] = '\0';
|
||||
if (back->url_sav != NULL && back->url_sav[0] != '\0') {
|
||||
snprintf(back->tmpfile_buffer, sizeof(back->tmpfile_buffer), "%s.z",
|
||||
back->url_sav);
|
||||
snprintf(back->tmpfile_buffer, sizeof(back->tmpfile_buffer), "%s.%s",
|
||||
back->url_sav, ext);
|
||||
back->tmpfile = back->tmpfile_buffer;
|
||||
if (structcheck(back->tmpfile) != 0) {
|
||||
hts_log_print(opt, LOG_WARNING, "can not create directory to %s",
|
||||
hts_log_print(opt, LOG_WARNING, "can not create directory to %s",
|
||||
back->tmpfile);
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
snprintf(back->tmpfile_buffer, sizeof(back->tmpfile_buffer),
|
||||
"%s/tmp%d.z", StringBuff(opt->path_html_utf8),
|
||||
opt->state.tmpnameid++);
|
||||
snprintf(back->tmpfile_buffer, sizeof(back->tmpfile_buffer), "%s/tmp%d.%s",
|
||||
StringBuff(opt->path_html_utf8), opt->state.tmpnameid++, ext);
|
||||
back->tmpfile = back->tmpfile_buffer;
|
||||
}
|
||||
/* OK */
|
||||
@@ -547,6 +556,32 @@ static int create_back_tmpfile(httrackp * opt, lien_back *const back) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Commit or restore a re-fetch backup (#77 follow-up): a re-fetch over an
|
||||
existing file moved the good copy to back->tmpfile before truncating url_sav.
|
||||
commit keeps the new file and drops the backup; else restore it so an aborted
|
||||
transfer leaves the previous copy intact. Skips the zlib .z temp. */
|
||||
static void back_finalize_backup(httrackp *opt, lien_back *const back,
|
||||
const hts_boolean commit) {
|
||||
if (back->tmpfile == NULL || back->r.compressed)
|
||||
return;
|
||||
if (commit) {
|
||||
(void) UNLINK(back->tmpfile); /* new copy is good; drop the backup */
|
||||
} else {
|
||||
if (back->r.out != NULL) {
|
||||
fclose(back->r.out);
|
||||
back->r.out = NULL;
|
||||
}
|
||||
(void) UNLINK(back->url_sav); /* drop the failed partial */
|
||||
/* On rename failure keep the backup: it still holds the good copy, so
|
||||
losing it would be worse than an orphaned temp. */
|
||||
if (RENAME(back->tmpfile, back->url_sav) != 0)
|
||||
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
|
||||
"could not restore %s; previous copy kept as %s",
|
||||
back->url_sav, back->tmpfile);
|
||||
}
|
||||
back->tmpfile = NULL;
|
||||
}
|
||||
|
||||
// objet (lien) téléchargé ou transféré depuis le cache
|
||||
//
|
||||
// fermer les paramètres de transfert,
|
||||
@@ -582,6 +617,7 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
|
||||
LLintP " got " LLintP "): %s%s", back[p].r.totalsize,
|
||||
back[p].r.size, back[p].url_adr, back[p].url_fil);
|
||||
}
|
||||
back_finalize_backup(opt, &back[p], HTS_FALSE);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -600,7 +636,7 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
|
||||
// en mémoire -> passage sur disque
|
||||
if (!back[p].r.is_write) {
|
||||
// do not use tempnam() but a regular filename
|
||||
if (create_back_tmpfile(opt, &back[p]) == 0) {
|
||||
if (create_back_tmpfile(opt, &back[p], "z") == 0) {
|
||||
assertf(back[p].tmpfile != NULL);
|
||||
/* note: tmpfile is utf-8 */
|
||||
back[p].r.out = FOPEN(back[p].tmpfile, "wb");
|
||||
@@ -673,6 +709,9 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
|
||||
}
|
||||
}
|
||||
#endif
|
||||
/* Body fully received: keep the freshly written url_sav, drop the
|
||||
backup of the previous copy. */
|
||||
back_finalize_backup(opt, &back[p], HTS_TRUE);
|
||||
/* Write mode to disk */
|
||||
if (back[p].r.is_write && back[p].r.adr != NULL) {
|
||||
freet(back[p].r.adr);
|
||||
@@ -901,6 +940,9 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Aborted, error, or not ready: url_sav (if written) is broken; restore the
|
||||
previous copy from the backup. */
|
||||
back_finalize_backup(opt, &back[p], HTS_FALSE);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -2050,13 +2092,9 @@ int back_add(struct_back * sback, httrackp * opt, cache_back * cache, const char
|
||||
|
||||
if (!back_trylive(opt, cache, sback, p)) {
|
||||
#if HTS_XGETHOST
|
||||
#if HDEBUG
|
||||
printf("back_solve..\n");
|
||||
#endif
|
||||
back[p].status = STATUS_WAIT_DNS; // tentative de résolution du nom de host
|
||||
soc = INVALID_SOCKET; // pas encore ouverte
|
||||
back_solve(opt, &back[p]); // préparer
|
||||
if (host_wait(opt, &back[p])) { // prêt, par ex fichier ou dispo dans dns
|
||||
back[p].status = STATUS_WAIT_DNS; // host name resolution attempt
|
||||
soc = INVALID_SOCKET; // not opened yet
|
||||
if (host_wait(opt, &back[p])) { // ready (file, or cached dns)
|
||||
#if HDEBUG
|
||||
printf("ok, dns cache ready..\n");
|
||||
#endif
|
||||
@@ -2199,37 +2237,9 @@ int back_add(struct_back * sback, httrackp * opt, cache_back * cache, const char
|
||||
}
|
||||
|
||||
#if HTS_XGETHOST
|
||||
// attendre que le host (ou celui du proxy) ait été résolu
|
||||
// si c'est un fichier, la résolution est immédiate
|
||||
// idem pour ftp://
|
||||
void back_solve(httrackp * opt, lien_back * back) {
|
||||
assertf(opt != NULL);
|
||||
assertf(back != NULL);
|
||||
if ((!strfield(back->url_adr, "file://"))
|
||||
&& !strfield(back->url_adr, "ftp://")
|
||||
) {
|
||||
const char *a;
|
||||
|
||||
if (!(back->r.req.proxy.active))
|
||||
a = back->url_adr;
|
||||
else
|
||||
a = back->r.req.proxy.name;
|
||||
assertf(a != NULL);
|
||||
a = jump_protocol_const(a);
|
||||
if (check_hostname_dns(a)) {
|
||||
hts_log_print(opt, LOG_DEBUG, "resolved: %s", a);
|
||||
} else {
|
||||
hts_log_print(opt, LOG_DEBUG, "failed to resolve: %s", a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// détermine si le host a pu être résolu
|
||||
int host_wait(httrackp * opt, lien_back * back) {
|
||||
// Always synchronous. No more background DNS resolution
|
||||
// (does not really improve performances)
|
||||
return 1;
|
||||
}
|
||||
// Resolution is synchronous inside the connect path; no pre-resolve step, so
|
||||
// the host is always immediately ready.
|
||||
int host_wait(httrackp *opt, lien_back *back) { return 1; }
|
||||
#endif
|
||||
|
||||
// élimine les fichiers non html en backing (anticipation)
|
||||
@@ -2416,9 +2426,15 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
|
||||
back_clean(opt, cache, sback);
|
||||
#endif
|
||||
|
||||
/* Time limit exceeded past grace: abort in-flight transfers so no wait loop
|
||||
starves (#481). FTP slots stay, their thread owns the socket. */
|
||||
/* Time/size limit exceeded past grace: abort in-flight transfers so no wait
|
||||
loop starves (#481, #77). FTP slots stay, their thread owns the socket. */
|
||||
if (!back_checkmirror(opt)) {
|
||||
const hts_mirror_limit limit = back_mirror_limit(opt);
|
||||
const char *const reason =
|
||||
(limit == HTS_MIRROR_LIMIT_SIZE) ? "size limit" : "time limit";
|
||||
const char *const slotmsg = (limit == HTS_MIRROR_LIMIT_SIZE)
|
||||
? "Mirror Size Limit"
|
||||
: "Mirror Time Out";
|
||||
int aborted = 0;
|
||||
unsigned int i;
|
||||
|
||||
@@ -2432,15 +2448,15 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
|
||||
if (back[i].r.is_write && IS_DELAYED_EXT(back[i].url_sav))
|
||||
back_delayed_discard(opt, &back[i]);
|
||||
back[i].r.statuscode = STATUSCODE_TIMEOUT;
|
||||
strcpybuff(back[i].r.msg, "Mirror Time Out");
|
||||
strcpybuff(back[i].r.msg, slotmsg);
|
||||
back[i].status = STATUS_READY;
|
||||
back_set_finished(sback, i);
|
||||
aborted++;
|
||||
}
|
||||
}
|
||||
if (aborted > 0)
|
||||
hts_log_print(opt, LOG_WARNING,
|
||||
"time limit reached, %d transfer(s) aborted", aborted);
|
||||
hts_log_print(opt, LOG_WARNING, "%s reached, %d transfer(s) aborted",
|
||||
reason, aborted);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2930,7 +2946,8 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
|
||||
strfield(get_ext(catbuff, sizeof(catbuff), back[i].url_sav), "gz") == 0
|
||||
&& strfield(get_ext(catbuff, sizeof(catbuff), back[i].url_sav), "tgz") == 0
|
||||
) {
|
||||
if (create_back_tmpfile(opt, &back[i]) == 0) {
|
||||
if (create_back_tmpfile(opt, &back[i], "z") ==
|
||||
0) {
|
||||
assertf(back[i].tmpfile != NULL);
|
||||
/* note: tmpfile is utf-8 */
|
||||
if ((back[i].r.out =
|
||||
@@ -2943,6 +2960,20 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
|
||||
back[i].url_sav, 1, 1,
|
||||
back[i].r.notmodified);
|
||||
back[i].r.compressed = 0;
|
||||
/* Re-fetch over an existing file (#77 follow-up):
|
||||
move the good copy aside before truncating it
|
||||
so an aborted transfer can restore it. url_sav
|
||||
is still written normally (file list intact).
|
||||
*/
|
||||
back[i].tmpfile = NULL;
|
||||
if (fexist_utf8(back[i].url_sav)) {
|
||||
if (create_back_tmpfile(opt, &back[i], "bak") !=
|
||||
0 ||
|
||||
RENAME(back[i].url_sav, back[i].tmpfile) !=
|
||||
0) {
|
||||
back[i].tmpfile = NULL;
|
||||
}
|
||||
}
|
||||
if ((back[i].r.out =
|
||||
filecreate(&opt->state.strc,
|
||||
back[i].url_sav)) == NULL) {
|
||||
@@ -3481,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
|
||||
*/
|
||||
@@ -3509,22 +3545,20 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
|
||||
if (back[i].r.statuscode == 406) { // 'Not Acceptable'
|
||||
back[i].r.statuscode = HTTP_OK;
|
||||
}
|
||||
// 'do not erase already downloaded file'
|
||||
// on an updated file
|
||||
// with an error : consider a 304 error
|
||||
if (!opt->delete_old) {
|
||||
if (HTTP_IS_ERROR(back[i].r.statuscode) && back[i].is_update
|
||||
&& !back[i].testmode) {
|
||||
if (back[i].url_sav[0] && fexist_utf8(back[i].url_sav)) {
|
||||
hts_log_print(opt, LOG_DEBUG,
|
||||
"Error ignored %d (%s) because of 'no purge' option for %s%s",
|
||||
back[i].r.statuscode, back[i].r.msg,
|
||||
back[i].url_adr, back[i].url_fil);
|
||||
back[i].r.statuscode = HTTP_NOT_MODIFIED;
|
||||
deletehttp(&back[i].r);
|
||||
back[i].r.soc = INVALID_SOCKET;
|
||||
}
|
||||
}
|
||||
// On update, keep the good copy on error (mask as 304); skip
|
||||
// resume paths so a stale-partial 416 still re-fetches.
|
||||
if (HTTP_IS_ERROR(back[i].r.statuscode) &&
|
||||
back[i].is_update && !back[i].testmode &&
|
||||
back[i].range_req_size == 0 && back[i].url_sav[0] &&
|
||||
fexist_utf8(back[i].url_sav)) {
|
||||
hts_log_print(opt, LOG_DEBUG,
|
||||
"Error %d (%s) ignored on update, keeping "
|
||||
"existing copy: %s%s",
|
||||
back[i].r.statuscode, back[i].r.msg,
|
||||
back[i].url_adr, back[i].url_fil);
|
||||
back[i].r.statuscode = HTTP_NOT_MODIFIED;
|
||||
deletehttp(&back[i].r);
|
||||
back[i].r.soc = INVALID_SOCKET;
|
||||
}
|
||||
// Various hacks to limit re-transfers when updating a mirror
|
||||
// Force update if same size detected
|
||||
@@ -3577,7 +3611,7 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
|
||||
if (back[i].r.statuscode == HTTP_OK && !back[i].testmode) { // 'OK'
|
||||
if (!is_hypertext_mime(opt, back[i].r.contenttype, back[i].url_fil)) { // not HTML
|
||||
if (strnotempty(back[i].url_sav)) { // target found
|
||||
int size = fsize_utf8(back[i].url_sav); // target size
|
||||
off_t size = fsize_utf8(back[i].url_sav);
|
||||
|
||||
if (size >= 0) {
|
||||
if (back[i].r.totalsize == size) { // same size!
|
||||
@@ -3672,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é
|
||||
@@ -3790,7 +3839,7 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
|
||||
const hts_boolean range_ok =
|
||||
back[i].r.crange > 0 && resume >= 0 &&
|
||||
resume <= (LLint) sz &&
|
||||
back[i].r.crange_end + 1 == back[i].r.crange &&
|
||||
back[i].r.crange_end == back[i].r.crange - 1 &&
|
||||
(back[i].r.totalsize < 0 ||
|
||||
back[i].r.totalsize ==
|
||||
back[i].r.crange_end - resume + 1);
|
||||
@@ -3859,11 +3908,20 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
|
||||
if (fp) {
|
||||
LLint alloc_mem = resume + 1;
|
||||
|
||||
if (back[i].r.totalsize >= 0)
|
||||
// Bound the in-memory buffer to a 32-bit size (real
|
||||
// in-RAM resources are far smaller); a hostile
|
||||
// Content-Length that would overflow the add or the
|
||||
// (size_t) cast is dropped and refetched instead.
|
||||
if (back[i].r.totalsize > INT32_MAX - alloc_mem) {
|
||||
url_savename_refname_remove(opt, back[i].url_adr,
|
||||
back[i].url_fil);
|
||||
UNLINK(back[i].url_sav);
|
||||
alloc_mem = -1;
|
||||
} else if (back[i].r.totalsize >= 0)
|
||||
alloc_mem += back[i].r.totalsize; // AJOUTER RESTANT!
|
||||
if (deleteaddr(&back[i].r)
|
||||
&& (back[i].r.adr =
|
||||
(char *) malloct((size_t) alloc_mem))) {
|
||||
if (alloc_mem >= 0 && deleteaddr(&back[i].r) &&
|
||||
(back[i].r.adr =
|
||||
(char *) malloct((size_t) alloc_mem))) {
|
||||
back[i].r.size = resume;
|
||||
if (back[i].r.totalsize >= 0)
|
||||
back[i].r.totalsize += resume; // -> full size
|
||||
@@ -4145,38 +4203,46 @@ static int back_maxtime_grace(const int maxtime) {
|
||||
return maximum(5, minimum(30, maxtime / 10));
|
||||
}
|
||||
|
||||
int back_checkmirror(httrackp * opt) {
|
||||
// Check max size
|
||||
if ((opt->maxsite > 0) && (HTS_STAT.stat_bytes >= opt->maxsite)) {
|
||||
if (!opt->state.stop) { /* not yet stopped */
|
||||
hts_log_print(opt, LOG_ERROR,
|
||||
"More than " LLintP
|
||||
" bytes have been transferred.. giving up",
|
||||
(LLint) opt->maxsite);
|
||||
/* cancel mirror smoothly */
|
||||
hts_request_stop(opt, 0);
|
||||
}
|
||||
return 1; /* don'k break mirror too sharply for size limits, but stop requested */
|
||||
/*return 0;
|
||||
*/
|
||||
}
|
||||
// Check max time
|
||||
/* Bytes the smooth stop may overrun before in-flight transfers are aborted.
|
||||
No floor (unlike the time grace): a size overrun should abort promptly. */
|
||||
static LLint back_maxsize_grace(const LLint maxsite) { return maxsite / 10; }
|
||||
|
||||
/* Which cap has overrun its grace and must hard-stop the mirror (#77, #481).
|
||||
-M measures received volume (HTS_TOTAL_RECV), not saved 200-only stat_bytes
|
||||
which undercounts redirect/error-heavy crawls (#520). */
|
||||
static hts_mirror_limit back_mirror_limit(httrackp *opt) {
|
||||
if (opt->maxsite > 0 && HTS_STAT.HTS_TOTAL_RECV >= opt->maxsite &&
|
||||
HTS_STAT.HTS_TOTAL_RECV - opt->maxsite >=
|
||||
back_maxsize_grace(opt->maxsite))
|
||||
return HTS_MIRROR_LIMIT_SIZE;
|
||||
if (opt->maxtime > 0) {
|
||||
const TStamp elapsed = time_local() - HTS_STAT.stat_timestart;
|
||||
|
||||
if (elapsed >= opt->maxtime) {
|
||||
if (!opt->state.stop) { /* not yet stopped */
|
||||
hts_log_print(opt, LOG_ERROR, "More than %d seconds passed.. giving up",
|
||||
opt->maxtime);
|
||||
/* cancel mirror smoothly */
|
||||
hts_request_stop(opt, 0);
|
||||
}
|
||||
/* smooth stop starved past the grace period: stop waiting (#481) */
|
||||
if (elapsed - opt->maxtime >= back_maxtime_grace(opt->maxtime))
|
||||
return 0;
|
||||
}
|
||||
if (elapsed >= opt->maxtime &&
|
||||
elapsed - opt->maxtime >= back_maxtime_grace(opt->maxtime))
|
||||
return HTS_MIRROR_LIMIT_TIME;
|
||||
}
|
||||
return 1; /* Ok, go on */
|
||||
return HTS_MIRROR_LIMIT_NONE;
|
||||
}
|
||||
|
||||
int back_checkmirror(httrackp *opt) {
|
||||
/* request a smooth stop the first time each cap is reached */
|
||||
if (opt->maxsite > 0 && HTS_STAT.HTS_TOTAL_RECV >= opt->maxsite &&
|
||||
!opt->state.stop) {
|
||||
hts_log_print(opt, LOG_ERROR,
|
||||
"More than " LLintP
|
||||
" bytes have been transferred.. giving up",
|
||||
(LLint) opt->maxsite);
|
||||
hts_request_stop(opt, 0);
|
||||
}
|
||||
if (opt->maxtime > 0 && !opt->state.stop &&
|
||||
(time_local() - HTS_STAT.stat_timestart) >= opt->maxtime) {
|
||||
hts_log_print(opt, LOG_ERROR, "More than %d seconds passed.. giving up",
|
||||
opt->maxtime);
|
||||
hts_request_stop(opt, 0);
|
||||
}
|
||||
/* hard stop once a cap overruns its grace (callers must stop waiting) */
|
||||
return back_mirror_limit(opt) == HTS_MIRROR_LIMIT_NONE;
|
||||
}
|
||||
|
||||
// octets transférés + add
|
||||
|
||||
@@ -138,12 +138,11 @@ LLint back_transferred(LLint add, struct_back * sback);
|
||||
|
||||
// hostback
|
||||
#if HTS_XGETHOST
|
||||
void back_solve(httrackp * opt, lien_back * sback);
|
||||
int host_wait(httrackp * opt, lien_back * sback);
|
||||
#endif
|
||||
int back_checksize(httrackp * opt, lien_back * eback, int check_only_totalsize);
|
||||
/* Enforce -M/-E quotas: requests a smooth stop when reached; returns 0 once
|
||||
the -E deadline overran its grace period (callers must stop waiting). */
|
||||
/* Enforce -M/-E quotas: smooth-stops when reached; returns 0 once the -M cap
|
||||
or -E deadline overruns its grace period (callers must stop waiting). */
|
||||
int back_checkmirror(httrackp * opt);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1904,8 +1904,11 @@ int httpmirror(char *url1, httrackp * opt) {
|
||||
}
|
||||
}
|
||||
|
||||
// ATTENTION C'EST ICI QU'ON SAUVE LE FICHIER!!
|
||||
if (r.adr != NULL || r.size == 0) {
|
||||
// ATTENTION C'EST ICI QU'ON SAUVE LE FICHIER!!
|
||||
// An empty body must not overwrite the file when the transfer failed
|
||||
// (statuscode <= 0, e.g. an -M hard-stop): it would truncate a good
|
||||
// copy to 0 (#77 follow-up).
|
||||
if (r.adr != NULL || (r.size == 0 && r.statuscode > 0)) {
|
||||
file_notify(opt, urladr(), urlfil(), savename(), 1, 1, r.notmodified);
|
||||
if (filesave(opt, r.adr, (int) r.size, savename(), urladr(), urlfil()) !=
|
||||
0) {
|
||||
@@ -1926,7 +1929,6 @@ int httpmirror(char *url1, httrackp * opt) {
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Parsing of other media types (java, ram..) */
|
||||
|
||||
@@ -297,6 +297,26 @@ int dns_selftests(httrackp *opt) {
|
||||
IPV6_resolver = 0;
|
||||
}
|
||||
|
||||
/* "host:port" resolves as the bare host: the cache/connect path strips
|
||||
":port" before both the backend and the cache key (#181). */
|
||||
mock_reset_calls();
|
||||
{
|
||||
SOCaddr a;
|
||||
char ip[64];
|
||||
const char *err = NULL;
|
||||
|
||||
/* cache miss: the backend is hit with the stripped host, not "host:port" */
|
||||
CHECK(hts_dns_resolve2(opt, "v6only.test:8080", &a, &err) != NULL);
|
||||
CHECK(mock_find("v6only.test")->calls == 1);
|
||||
/* the bare host shares that one cache entry (stripped key) */
|
||||
CHECK(hts_dns_resolve2(opt, "v6only.test", &a, &err) != NULL);
|
||||
CHECK(mock_find("v6only.test")->calls == 1);
|
||||
/* a port variant of an already-cached host also hits, right address */
|
||||
CHECK(hts_dns_resolve2(opt, "v4only.test:1234", &a, &err) != NULL);
|
||||
SOCaddr_inetntoa(ip, sizeof(ip), a);
|
||||
CHECK(strcmp(ip, "1.2.3.4") == 0);
|
||||
}
|
||||
|
||||
/* newhttp_addr() must connect to the addr_index-th address, not always the
|
||||
first: this is what back_connect_next relies on to reach the fallback. */
|
||||
{
|
||||
|
||||
@@ -56,6 +56,10 @@ Please visit our Website: http://www.httrack.com
|
||||
// Platform detection (sizes, feature macros)
|
||||
#include "htsconfig.h"
|
||||
|
||||
// Fixed-width integer types + PRI* format macros for the LLint/TStamp typedefs
|
||||
#include <stdint.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
// WIN32 types
|
||||
#ifdef _WIN32
|
||||
#ifndef SIZEOF_LONG
|
||||
@@ -111,29 +115,6 @@ Please visit our Website: http://www.httrack.com
|
||||
#define HTS_DO_NOT_USE_UID
|
||||
#endif
|
||||
|
||||
#ifndef HTS_LONGLONG
|
||||
#ifdef SIZEOF_LONG_LONG
|
||||
#if SIZEOF_LONG_LONG == 8
|
||||
#define HTS_LONGLONG 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef HTS_LONGLONG
|
||||
#ifdef __sun
|
||||
#define HTS_LONGLONG 0
|
||||
#endif
|
||||
#ifdef __osf__
|
||||
#define HTS_LONGLONG 0
|
||||
#endif
|
||||
#ifdef __linux
|
||||
#define HTS_LONGLONG 1
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
#define HTS_LONGLONG 1
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef DLLIB
|
||||
#define HTS_DLOPEN 1
|
||||
#else
|
||||
@@ -326,59 +307,11 @@ typedef int hts_tristate;
|
||||
#define HTS_DEPRECATED(msg)
|
||||
#endif
|
||||
|
||||
#ifndef HTS_LONGLONG
|
||||
#ifdef HTS_NO_64_BIT
|
||||
#define HTS_LONGLONG 0
|
||||
#else
|
||||
#define HTS_LONGLONG 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* Wide integer types, chosen per platform.
|
||||
LLint: signed 64-bit counter for byte counts and large sizes (falls back to
|
||||
plain int where 64-bit is unavailable).
|
||||
TStamp: timestamp/duration in the same width (a double in the no-64-bit
|
||||
fallback).
|
||||
LLintP: the printf conversion for an LLint. */
|
||||
#if HTS_LONGLONG
|
||||
#ifdef LLINT_FORMAT
|
||||
typedef LLINT_TYPE LLint;
|
||||
|
||||
typedef LLINT_TYPE TStamp;
|
||||
|
||||
#define LLintP LLINT_FORMAT
|
||||
#else
|
||||
|
||||
#ifdef _WIN32
|
||||
typedef __int64 LLint;
|
||||
|
||||
typedef __int64 TStamp;
|
||||
|
||||
#define LLintP "%I64d"
|
||||
#elif (defined(_LP64) || defined(__x86_64__) || defined(__powerpc64__) || \
|
||||
defined(__64BIT__))
|
||||
|
||||
typedef long int LLint;
|
||||
|
||||
typedef long int TStamp;
|
||||
|
||||
#define LLintP "%ld"
|
||||
#else
|
||||
typedef long long int LLint;
|
||||
|
||||
typedef long long int TStamp;
|
||||
|
||||
#define LLintP "%lld"
|
||||
#endif
|
||||
|
||||
#endif /* HTS_LONGLONG */
|
||||
|
||||
#else
|
||||
typedef int LLint;
|
||||
|
||||
#define LLintP "%d"
|
||||
typedef double TStamp;
|
||||
#endif
|
||||
/* LLint/TStamp: signed exact-width 64-bit; -1 is a sentinel engine-wide. */
|
||||
typedef int64_t LLint;
|
||||
typedef int64_t TStamp;
|
||||
/* Full printf conversion, '%' included (PRId64 has none): "X: " LLintP. */
|
||||
#define LLintP "%" PRId64
|
||||
|
||||
/* Integer type for file offsets/sizes passed to the C library. Widens to
|
||||
LLint (with HTS_FSEEKO for fseeko/ftello) under large-file support, plain
|
||||
|
||||
47
src/htslib.c
47
src/htslib.c
@@ -1059,10 +1059,10 @@ static int append_cookie_header(buff_struct *bstr, t_cookie *cookie,
|
||||
return cook;
|
||||
}
|
||||
|
||||
/* Self-test entry for append_cookie_header(): build the request Cookie line
|
||||
into dst (always NUL-terminated). Returns the number of cookies emitted. */
|
||||
int http_cookie_header_selftest(t_cookie *cookie, const char *domain,
|
||||
const char *path, char *dst, size_t dst_size) {
|
||||
/* Build the request Cookie line for domain/path into dst (always
|
||||
NUL-terminated). Returns the number of cookies emitted. */
|
||||
int http_cookie_header(t_cookie *cookie, const char *domain, const char *path,
|
||||
char *dst, size_t dst_size) {
|
||||
buff_struct bstr = {dst, dst_size, 0};
|
||||
|
||||
assertf(dst != NULL && dst_size > 0);
|
||||
@@ -1586,7 +1586,7 @@ void treathead(t_cookie * cookie, const char *adr, const char *fil, htsblk * ret
|
||||
a = strchr(rcvd + p, '/');
|
||||
if (a != NULL) {
|
||||
a++;
|
||||
if (sscanf(a, LLintP, &retour->crange) == 1) {
|
||||
if (sscanf(a, LLintP, &retour->crange) == 1 && retour->crange >= 0) {
|
||||
retour->crange_start = 0;
|
||||
retour->crange_end = retour->crange - 1;
|
||||
} else {
|
||||
@@ -1594,6 +1594,12 @@ void treathead(t_cookie * cookie, const char *adr, const char *fil, htsblk * ret
|
||||
}
|
||||
}
|
||||
}
|
||||
// A valid Content-Range has no negative field; reject hostile values so
|
||||
// the crange +/- 1 arithmetic downstream cannot sign-overflow (UB).
|
||||
if (retour->crange_start < 0 || retour->crange_end < 0 ||
|
||||
retour->crange < 0) {
|
||||
retour->crange_start = retour->crange_end = retour->crange = 0;
|
||||
}
|
||||
}
|
||||
} else if ((p = strfield(rcvd, "Connection:")) != 0) {
|
||||
char *a = rcvd + p;
|
||||
@@ -1998,6 +2004,15 @@ LLint http_xfread1(htsblk * r, int bufl) {
|
||||
|
||||
if (bufl > 0) {
|
||||
if (!r->is_write) { // stocker en mémoire
|
||||
// In-memory content must fit a 32-bit index (allocs below add 1, reads
|
||||
// use int offsets): reject a hostile Content-Length or endless stream.
|
||||
const LLint inmem_want =
|
||||
(r->totalsize >= 0) ? r->totalsize : (r->size + bufl);
|
||||
if (inmem_want >= INT32_MAX) {
|
||||
r->statuscode = STATUSCODE_INVALID;
|
||||
strcpybuff(r->msg, "In-memory content too large");
|
||||
return READ_ERROR;
|
||||
}
|
||||
if (r->totalsize >= 0) { // totalsize déterminé ET ALLOUE
|
||||
if (r->adr == NULL) {
|
||||
r->adr = (char *) malloct((size_t) r->totalsize + 1);
|
||||
@@ -3038,12 +3053,11 @@ HTSEXT_API char *int2char(strc_int2bytes2 * strc, int n) {
|
||||
/* See http://physics.nist.gov/cuu/Units/binary.html */
|
||||
#define ToLLint(a) ((LLint)(a))
|
||||
#define ToLLintKiB (ToLLint(1024))
|
||||
#define ToLLintMiB (ToLLintKiB*ToLLintKiB)
|
||||
#ifdef HTS_LONGLONG
|
||||
#define ToLLintGiB (ToLLintKiB*ToLLintKiB*ToLLintKiB)
|
||||
#define ToLLintTiB (ToLLintKiB*ToLLintKiB*ToLLintKiB*ToLLintKiB)
|
||||
#define ToLLintPiB (ToLLintKiB*ToLLintKiB*ToLLintKiB*ToLLintKiB*ToLLintKiB)
|
||||
#endif
|
||||
#define ToLLintMiB (ToLLintKiB * ToLLintKiB)
|
||||
#define ToLLintGiB (ToLLintKiB * ToLLintKiB * ToLLintKiB)
|
||||
#define ToLLintTiB (ToLLintKiB * ToLLintKiB * ToLLintKiB * ToLLintKiB)
|
||||
#define ToLLintPiB \
|
||||
(ToLLintKiB * ToLLintKiB * ToLLintKiB * ToLLintKiB * ToLLintKiB)
|
||||
HTSEXT_API char **int2bytes2(strc_int2bytes2 * strc, LLint n) {
|
||||
if (n < ToLLintKiB) {
|
||||
sprintf(strc->buff1, "%d", (int) (LLint) n);
|
||||
@@ -3052,9 +3066,7 @@ HTSEXT_API char **int2bytes2(strc_int2bytes2 * strc, LLint n) {
|
||||
sprintf(strc->buff1, "%d,%02d", (int) ((LLint) (n / ToLLintKiB)),
|
||||
(int) ((LLint) ((n % ToLLintKiB) * 100) / ToLLintKiB));
|
||||
strcpybuff(strc->buff2, "KiB");
|
||||
}
|
||||
#ifdef HTS_LONGLONG
|
||||
else if (n < ToLLintGiB) {
|
||||
} else if (n < ToLLintGiB) {
|
||||
sprintf(strc->buff1, "%d,%02d", (int) ((LLint) (n / (ToLLintMiB))),
|
||||
(int) ((LLint) (((n % (ToLLintMiB)) * 100) / (ToLLintMiB))));
|
||||
strcpybuff(strc->buff2, "MiB");
|
||||
@@ -3071,13 +3083,6 @@ HTSEXT_API char **int2bytes2(strc_int2bytes2 * strc, LLint n) {
|
||||
(int) ((LLint) (((n % (ToLLintPiB)) * 100) / (ToLLintPiB))));
|
||||
strcpybuff(strc->buff2, "PiB");
|
||||
}
|
||||
#else
|
||||
else {
|
||||
sprintf(strc->buff1, "%d,%02d", (int) ((LLint) (n / (ToLLintMiB))),
|
||||
(int) ((LLint) (((n % (ToLLintMiB)) * 100) / (ToLLintMiB))));
|
||||
strcpybuff(strc->buff2, "MiB");
|
||||
}
|
||||
#endif
|
||||
strc->buffadr[0] = strc->buff1;
|
||||
strc->buffadr[1] = strc->buff2;
|
||||
return strc->buffadr;
|
||||
|
||||
@@ -184,10 +184,10 @@ int http_sendhead(httrackp * opt, t_cookie * cookie, int mode, const char *xsend
|
||||
const char *referer_adr, const char *referer_fil,
|
||||
htsblk * retour);
|
||||
/* Build the request "Cookie:" header line for stored cookies matching
|
||||
domain/path into dst (NUL-terminated). Exposed for the -#Q self-test;
|
||||
wraps the same logic http_sendhead() uses. Returns cookies emitted. */
|
||||
int http_cookie_header_selftest(t_cookie *cookie, const char *domain,
|
||||
const char *path, char *dst, size_t dst_size);
|
||||
domain/path into dst (NUL-terminated), wrapping the logic http_sendhead()
|
||||
uses. Returns cookies emitted. */
|
||||
int http_cookie_header(t_cookie *cookie, const char *domain, const char *path,
|
||||
char *dst, size_t dst_size);
|
||||
|
||||
T_SOC newhttp(httrackp * opt, const char *iadr, htsblk * retour, int port,
|
||||
int waitconnect);
|
||||
|
||||
@@ -4311,6 +4311,8 @@ int hts_wait_delayed(htsmoduleStruct * str, lien_adrfilsave *afs,
|
||||
*forbidden_url == 0 && IS_DELAYED_EXT(afs->save) && !opt->state.stop) {
|
||||
int loops;
|
||||
int continue_loop;
|
||||
char BIGSTK
|
||||
cookie_before[16384]; /* #15: this URL's Cookie header, pre-request */
|
||||
|
||||
hts_log_print(opt, LOG_DEBUG, "Waiting for type to be known: %s%s", afs->af.adr,
|
||||
afs->af.fil);
|
||||
@@ -4319,6 +4321,12 @@ int hts_wait_delayed(htsmoduleStruct * str, lien_adrfilsave *afs,
|
||||
for(loops = 0, continue_loop = 1;
|
||||
IS_DELAYED_EXT(afs->save) && continue_loop && loops < 7; loops++) {
|
||||
continue_loop = 0;
|
||||
/* #15: snapshot the Cookie header THIS url would send, so only its own
|
||||
self-redirect Set-Cookie trips the retry, not a concurrent slot's. */
|
||||
cookie_before[0] = '\0';
|
||||
if (opt->accept_cookie && opt->cookie != NULL)
|
||||
http_cookie_header(opt->cookie, jump_identification_const(afs->af.adr),
|
||||
afs->af.fil, cookie_before, sizeof(cookie_before));
|
||||
|
||||
/* Wait for an available slot */
|
||||
if (!hts_wait_available_socket(sback, opt, cache, ptr))
|
||||
@@ -4516,6 +4524,17 @@ int hts_wait_delayed(htsmoduleStruct * str, lien_adrfilsave *afs,
|
||||
/* Moved! */
|
||||
else if (HTTP_IS_REDIRECT(back[b].r.statuscode)) {
|
||||
char BIGSTK mov_url[HTS_URLMAXSIZE * 2];
|
||||
char BIGSTK cookie_after[16384];
|
||||
hts_boolean cookies_changed;
|
||||
|
||||
/* #15: cookie-wall signal: this URL's own Cookie header changed
|
||||
across its self-redirect (a Set-Cookie it just set). */
|
||||
cookie_after[0] = '\0';
|
||||
if (opt->accept_cookie && opt->cookie != NULL)
|
||||
http_cookie_header(
|
||||
opt->cookie, jump_identification_const(afs->af.adr),
|
||||
afs->af.fil, cookie_after, sizeof(cookie_after));
|
||||
cookies_changed = strcmp(cookie_before, cookie_after) != 0;
|
||||
|
||||
mov_url[0] = '\0';
|
||||
strcpybuff(mov_url, back[b].r.location); // copier URL
|
||||
@@ -4585,11 +4604,24 @@ int hts_wait_delayed(htsmoduleStruct * str, lien_adrfilsave *afs,
|
||||
url_savename(afs, former, heap(ptr)->adr, heap(ptr)->fil,
|
||||
opt, sback, cache, hash, ptr, numero_passe,
|
||||
&delayed_back);
|
||||
} else if (cookies_changed) {
|
||||
// #15: cookie-wall self-redirect; evict the cached
|
||||
// fast-header so the re-issue refetches with the cookie.
|
||||
if (cache->cached_tests != NULL)
|
||||
coucal_remove(cache->cached_tests,
|
||||
concat(OPT_GET_BUFF(opt),
|
||||
OPT_GET_BUFF_SIZE(opt), afs->af.adr,
|
||||
afs->af.fil));
|
||||
afs->save[0] = '\0';
|
||||
url_savename(afs, former, heap(ptr)->adr, heap(ptr)->fil, opt,
|
||||
sback, cache, hash, ptr, numero_passe,
|
||||
&delayed_back);
|
||||
continue_loop = 1;
|
||||
} else {
|
||||
hts_log_print(opt, LOG_WARNING,
|
||||
"Unable to test %s%s (loop to same filename)",
|
||||
afs->af.adr, afs->af.fil);
|
||||
} // loop to same location
|
||||
} // loop to same location
|
||||
} // ident_url_relatif()
|
||||
} // location
|
||||
} // redirect
|
||||
|
||||
@@ -1310,6 +1310,82 @@ static int st_headerlong(httrackp *opt, int argc, char **argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* http_xfread1 must refuse an in-memory buffer whose size would exceed a 32-bit
|
||||
index (hostile Content-Length or endless stream) rather than allocate it.
|
||||
The guard returns before any socket read, so no real connection is needed. */
|
||||
static int st_xfread_limit(httrackp *opt, int argc, char **argv) {
|
||||
htsblk r;
|
||||
|
||||
(void) opt;
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
// Content-Length just over 2 GiB.
|
||||
memset(&r, 0, sizeof(r));
|
||||
r.soc = INVALID_SOCKET;
|
||||
r.totalsize = (LLint) INT32_MAX + 1;
|
||||
printf("bylen: refused=%d adr=%s msg=%s\n",
|
||||
http_xfread1(&r, 8192) == READ_ERROR, r.adr != NULL ? "alloc" : "null",
|
||||
r.msg);
|
||||
if (r.adr != NULL)
|
||||
freet(r.adr);
|
||||
|
||||
// Unknown length, buffer already at the limit: the next read would exceed it.
|
||||
memset(&r, 0, sizeof(r));
|
||||
r.soc = INVALID_SOCKET;
|
||||
r.totalsize = -1;
|
||||
r.size = (LLint) INT32_MAX;
|
||||
printf("bygrow: refused=%d adr=%s msg=%s\n",
|
||||
http_xfread1(&r, 8192) == READ_ERROR, r.adr != NULL ? "alloc" : "null",
|
||||
r.msg);
|
||||
if (r.adr != NULL)
|
||||
freet(r.adr);
|
||||
|
||||
// Exactly at the 2 GiB index (size + bufl == INT32_MAX): must also be
|
||||
// refused, since the reallocs below add 1 (a `> INT32_MAX` check would let
|
||||
// this through and overflow the int realloc size).
|
||||
memset(&r, 0, sizeof(r));
|
||||
r.soc = INVALID_SOCKET;
|
||||
r.totalsize = -1;
|
||||
r.size = (LLint) INT32_MAX - 8192;
|
||||
http_xfread1(&r, 8192);
|
||||
printf("boundary: msg=%s\n", r.msg);
|
||||
|
||||
// A legitimate small size must NOT be refused by the guard (the read then
|
||||
// fails on the invalid socket, but the size-too-large msg must not be set).
|
||||
memset(&r, 0, sizeof(r));
|
||||
r.soc = INVALID_SOCKET;
|
||||
r.totalsize = 1000;
|
||||
http_xfread1(&r, 8192);
|
||||
printf("accept: msg=%s\n", r.msg);
|
||||
if (r.adr != NULL)
|
||||
freet(r.adr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Parse a Content-Range header and print the sanitized triple. A hostile value
|
||||
(negative or INT64 extreme) must clamp to 0 without signed-overflow UB. */
|
||||
static int st_crange(httrackp *opt, int argc, char **argv) {
|
||||
int i;
|
||||
|
||||
(void) opt;
|
||||
if (argc < 1) {
|
||||
fprintf(stderr, "crange: needs at least one raw Content-Range line\n");
|
||||
return 1;
|
||||
}
|
||||
for (i = 0; i < argc; i++) {
|
||||
htsblk r;
|
||||
char BIGSTK line[HTS_URLMAXSIZE * 2];
|
||||
|
||||
memset(&r, 0, sizeof(r));
|
||||
strcpybuff(line, argv[i]);
|
||||
treathead(NULL, "www.example.com", "/", &r, line);
|
||||
printf("crange_start=" LLintP " crange_end=" LLintP " crange=" LLintP "\n",
|
||||
(LLint) r.crange_start, (LLint) r.crange_end, (LLint) r.crange);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Decode a body argument ("hex:FFD8.." or literal text) into buf. */
|
||||
static size_t st_decode_body(const char *arg, char *buf, size_t size) {
|
||||
size_t n = 0;
|
||||
@@ -1679,7 +1755,7 @@ static int st_cookies(httrackp *opt, int argc, char **argv) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
http_cookie_header_selftest(&cookie, dom, "/", hdr, sizeof(hdr));
|
||||
http_cookie_header(&cookie, dom, "/", hdr, sizeof(hdr));
|
||||
if (strcmp(hdr, expected) != 0)
|
||||
err = 1;
|
||||
if (strstr(hdr, "$Version") != NULL || strstr(hdr, "$Path") != NULL)
|
||||
@@ -2496,6 +2572,10 @@ static const struct selftest_entry {
|
||||
{"headerlong", "[header-name:]",
|
||||
"over-long header value must not overflow the parse scratch",
|
||||
st_headerlong},
|
||||
{"crange", "<raw-content-range-line> ...",
|
||||
"Content-Range parse integer safety", st_crange},
|
||||
{"xfread-limit", "", "in-memory receive buffer size bound",
|
||||
st_xfread_limit},
|
||||
{"savename", "<fil> <content-type> [key=value ...]",
|
||||
"local save-name for a URL", st_savename},
|
||||
{"sniff", "<content-type> <hex:..|text>", "MIME magic consistency",
|
||||
|
||||
34
tests/01_engine-crange.test
Executable file
34
tests/01_engine-crange.test
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Content-Range parse (treathead via -#test=crange). Hostile values must clamp
|
||||
# to 0, and the crange +/- 1 arithmetic must not sign-overflow (UBSan aborts on
|
||||
# the pre-fix binary at the INT64_MIN cases).
|
||||
|
||||
cr() {
|
||||
local want="$1"
|
||||
shift
|
||||
out="$(httrack -O /dev/null -#test=crange "$@")"
|
||||
test "$out" == "$want" || {
|
||||
echo "FAIL: $* -> '$out' (want '$want')"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Normal range and the '*/N' fallback both parse through unchanged.
|
||||
cr 'crange_start=0 crange_end=70870 crange=70871' 'Content-Range: bytes 0-70870/70871'
|
||||
cr 'crange_start=0 crange_end=70870 crange=70871' 'Content-Range: bytes */70871'
|
||||
|
||||
# INT64_MIN total in the fallback would make crange-1 overflow: clamp to 0.
|
||||
cr 'crange_start=0 crange_end=0 crange=0' 'Content-Range: bytes */-9223372036854775808'
|
||||
|
||||
# Any negative field in the 3-field form clamps the whole triple to 0 (each
|
||||
# field independently: a leading negative start/end alone still zeroes all).
|
||||
cr 'crange_start=0 crange_end=0 crange=0' 'Content-Range: bytes -1--1/-1'
|
||||
cr 'crange_start=0 crange_end=0 crange=0' 'Content-Range: bytes -5-10/20'
|
||||
|
||||
# INT64_MAX is non-negative: it passes through, the parse must not mangle it.
|
||||
cr 'crange_start=0 crange_end=9223372036854775807 crange=9223372036854775807' \
|
||||
'Content-Range: bytes 0-9223372036854775807/9223372036854775807'
|
||||
29
tests/01_engine-xfread.test
Executable file
29
tests/01_engine-xfread.test
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# http_xfread1 must refuse an in-memory receive buffer that would exceed a
|
||||
# 32-bit index (hostile Content-Length, or an endless stream) rather than
|
||||
# allocate it, while still accepting a normal small size. -#test=xfread-limit
|
||||
# drives both refusal paths and the accept path.
|
||||
|
||||
out="$(httrack -O /dev/null -#test=xfread-limit)"
|
||||
for case in bylen bygrow; do
|
||||
echo "$out" | grep -q "${case}: refused=1 adr=null msg=In-memory content too large" || {
|
||||
echo "FAIL ${case}: $out"
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
# Exactly INT32_MAX must be refused too (the reallocs add 1).
|
||||
echo "$out" | grep -q 'boundary: msg=In-memory content too large' || {
|
||||
echo "FAIL boundary: $out"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# The guard must NOT fire for a legitimate small size.
|
||||
if echo "$out" | grep -q 'accept: msg=In-memory content too large'; then
|
||||
echo "FAIL accept (guard fired on a legit size): $out"
|
||||
exit 1
|
||||
fi
|
||||
@@ -66,9 +66,12 @@ test -n "$port" || {
|
||||
}
|
||||
|
||||
out="$tmpdir/crawl"
|
||||
# macOS has only 127.0.0.1, so the dead 127.0.0.x connects block to the timeout
|
||||
# instead of refusing instantly (as on Linux); a small --timeout + --retries=0
|
||||
# bound the stall without changing what the fallback exercises.
|
||||
HTTRACK_DEBUG_RESOLVE="deadhost:127.0.0.2,127.0.0.1" \
|
||||
httrack "http://deadhost:$port/simple/basic.html" -O "$out" \
|
||||
-c1 --robots=0 --timeout=30 --quiet -Z >"$tmpdir/log" 2>&1
|
||||
-c1 --robots=0 --timeout=5 --retries=0 --quiet -Z >"$tmpdir/log" 2>&1
|
||||
|
||||
log="$out/hts-log.txt"
|
||||
|
||||
@@ -92,12 +95,12 @@ test -f "$out/deadhost_$port/simple/basic.html" || {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# every address refused: the slot exhausts the list, then errors out (the
|
||||
# harness timeout would catch a hang/loop; refused connects are instant)
|
||||
# every address dead: the slot exhausts the list, then errors out (the harness
|
||||
# timeout would catch a hang/loop)
|
||||
out2="$tmpdir/crawl2"
|
||||
HTTRACK_DEBUG_RESOLVE="alldead:127.0.0.2,127.0.0.3" \
|
||||
httrack "http://alldead:$port/simple/basic.html" -O "$out2" \
|
||||
-c1 --robots=0 --timeout=30 --quiet -Z >"$tmpdir/log2" 2>&1
|
||||
-c1 --robots=0 --timeout=5 --retries=0 --quiet -Z >"$tmpdir/log2" 2>&1
|
||||
log2="$out2/hts-log.txt"
|
||||
|
||||
grep -q "trying next address" "$log2" || {
|
||||
|
||||
@@ -17,7 +17,10 @@ printf 'GIF89a' >"$tmproot/javaclass/hello.gif"
|
||||
printf '\xCA\xFE\xBA\xBE\x00\x00\x00\x32\x00\x02\x01\x00\x09hello.gif\x00\x00\x00\x00' \
|
||||
>"$tmproot/javaclass/Foo.class"
|
||||
|
||||
# --log-found first: a plugin-load failure (wrong soname, shadowing system
|
||||
# libhtsjava) trips here on the banner suffix, not as an opaque "not found".
|
||||
bash "$top_srcdir/tests/local-crawl.sh" --root "$tmproot" --errors 0 \
|
||||
--log-found '\+libhtsjava' \
|
||||
--found 'javaclass/Foo.class' \
|
||||
--found 'javaclass/hello.gif' \
|
||||
httrack 'BASEURL/javaclass/index.html'
|
||||
|
||||
@@ -9,7 +9,7 @@ set -euo pipefail
|
||||
: "${top_srcdir:=..}"
|
||||
|
||||
bash "$top_srcdir/tests/local-crawl.sh" --rerun \
|
||||
--errors 4 --files 361 \
|
||||
--errors-content 4 --files 361 \
|
||||
--found 'big/p/95.html' \
|
||||
--found 'big/a/d1/d2/d3/d4/d5/d6/d7/d8/deep.png' \
|
||||
--found 'big/a/f2-2x.png' \
|
||||
@@ -53,4 +53,4 @@ bash "$top_srcdir/tests/local-crawl.sh" --rerun \
|
||||
--log-found ', no files updated' \
|
||||
--max-mirror-bytes 700000 \
|
||||
--min-mirror-bytes 500000 \
|
||||
httrack 'BASEURL/big/index.html' --retries=0 -c8 -%c100 -A100000000
|
||||
httrack 'BASEURL/big/index.html' --retries=0 -c16 -%c100 -A100000000
|
||||
|
||||
21
tests/42_local-maxsize-slow.test
Executable file
21
tests/42_local-maxsize-slow.test
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# -M byte cap under a slow server (#77): p0 overruns -M at once while p1..p3
|
||||
# trickle for a minute; the cap must abort the in-flight transfers, not wait
|
||||
# them out. Unfixed, the smooth stop drains the trickle at server pace.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${top_srcdir:=..}"
|
||||
|
||||
start=$(date +%s)
|
||||
bash "$top_srcdir/tests/local-crawl.sh" \
|
||||
--log-found 'More than 400000 bytes have been transferred.. giving up' \
|
||||
httrack 'BASEURL/bigtrickle/index.html' -M400000 -c4
|
||||
wall=$(($(date +%s) - start))
|
||||
# trickle runs 60s; a smooth-only stop waits it out, the hard stop lands near 8s.
|
||||
# Wall clock is the discriminating check: the log line above fires either way.
|
||||
if [ "$wall" -ge 30 ]; then
|
||||
echo "crawl took ${wall}s, -M hard stop did not engage" >&2
|
||||
exit 1
|
||||
fi
|
||||
24
tests/43_local-update-truncate.test
Normal file
24
tests/43_local-update-truncate.test
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# -M hard-abort must not destroy already-complete files (#77 follow-up).
|
||||
# Pass 1 mirrors bigtrunc fully. Pass 2 re-fetches under --update -M: fast.bin
|
||||
# overruns the cap, its abort tears down slow.bin's stalled re-fetch. Both were
|
||||
# complete on disk from pass 1, and both must survive the aborted update:
|
||||
# - slow.bin's re-fetch is incomplete; the direct-to-disk write must not be
|
||||
# left truncated (the previous copy is restored).
|
||||
# - fast.bin fully arrives but is torn down; the aborted slot must not overwrite
|
||||
# it with an empty body.
|
||||
# Unfixed, slow.bin is truncated and fast.bin is zeroed while the cache still
|
||||
# records them complete.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${top_srcdir:=..}"
|
||||
|
||||
bash "$top_srcdir/tests/local-crawl.sh" \
|
||||
--rerun-args '--update -M400000' \
|
||||
--log-found 'More than 400000 bytes have been transferred.. giving up' \
|
||||
--found bigtrunc/slow.bin \
|
||||
--file-min-bytes bigtrunc/slow.bin 655360 \
|
||||
--file-min-bytes bigtrunc/fast.bin 655360 \
|
||||
httrack 'BASEURL/bigtrunc/index.html' -c2
|
||||
13
tests/44_local-update-errormask.test
Normal file
13
tests/44_local-update-errormask.test
Normal file
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
# Issue #176: on --update a page that 200'd on the first crawl but now 403s must
|
||||
# keep its good local copy, not be overwritten by the error body nor purged.
|
||||
set -euo pipefail
|
||||
|
||||
: "${top_srcdir:=..}"
|
||||
|
||||
bash "$top_srcdir/tests/local-crawl.sh" --rerun \
|
||||
--found 'errmask/keep.dat' \
|
||||
--file-min-bytes 'errmask/keep.dat' 1024 \
|
||||
--file-matches 'errmask/keep.dat' '^KEEP' \
|
||||
--file-not-matches 'errmask/keep.dat' 'error 403' \
|
||||
httrack 'BASEURL/errmask/index.html'
|
||||
16
tests/45_local-maxsize-recv.test
Executable file
16
tests/45_local-maxsize-recv.test
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# -M caps received volume, not saved 200-only bytes (#520): links to large 404
|
||||
# bodies pump received >> saved; the cap must trip though little lands on disk.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${top_srcdir:=..}"
|
||||
|
||||
# The mirror bound is the load-bearing half: it fires while <100 KB is saved,
|
||||
# so the cap can only have tripped on received volume, not saved bytes (which
|
||||
# would exceed the cap and fetch all 16 links).
|
||||
bash "$top_srcdir/tests/local-crawl.sh" \
|
||||
--log-found 'More than 500000 bytes have been transferred.. giving up' \
|
||||
--max-mirror-bytes 100000 \
|
||||
httrack 'BASEURL/maxrecv/index.html' -M500000 -c4
|
||||
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"
|
||||
96
tests/47_local-crange-overflow.test
Executable file
96
tests/47_local-crange-overflow.test
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/bin/bash
|
||||
# C2: a resume whose 206 carries a Content-Range end of INT64_MAX must not
|
||||
# sign-overflow the crange+1 range check (UBSan aborts on the pre-fix binary).
|
||||
# Pass 1 leaves a partial + temp-ref; pass 2 resumes, gets the hostile 206, and
|
||||
# must reject the range and refetch the whole file rather than overflow. Teeth
|
||||
# are UBSan-only: a normal build wraps to the same reject+restart.
|
||||
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_crange.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"
|
||||
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}/crange206/index.html" >"${tmpdir}/log1" 2>&1 &
|
||||
crawlpid=$!
|
||||
for _ in $(seq 1 300); do
|
||||
test -n "$(find "$refdir" -name '*.ref' 2>/dev/null)" && break
|
||||
kill -0 "$crawlpid" 2>/dev/null || break
|
||||
sleep 0.1
|
||||
done
|
||||
sleep 0.3
|
||||
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 the resume"
|
||||
exit 1
|
||||
}
|
||||
echo "OK (temp-ref present)"
|
||||
|
||||
# --- pass 2: --continue -> resume -> hostile INT64_MAX Content-Range ----------
|
||||
printf '[pass 2: hostile 206, no overflow, refetch] ..\t'
|
||||
httrack "${common[@]}" --continue "${base}/crange206/index.html" >"${tmpdir}/log2" 2>&1
|
||||
echo "OK (terminated)"
|
||||
|
||||
blob=$(find "$out" -name blob.bin 2>/dev/null | head -1)
|
||||
full=6008 # len(CRANGE206_BODY) = len("CR206DAT") + 6000
|
||||
|
||||
printf '[file recovered whole] ..\t'
|
||||
test -s "$blob" || {
|
||||
echo "FAIL: blob.bin missing after the hostile 206"
|
||||
exit 1
|
||||
}
|
||||
got=$(wc -c <"$blob")
|
||||
test "$got" -eq "$full" || {
|
||||
echo "FAIL: blob.bin is ${got} bytes, expected ${full}"
|
||||
exit 1
|
||||
}
|
||||
echo "OK (${got} bytes)"
|
||||
96
tests/48_local-crange-memresume.test
Executable file
96
tests/48_local-crange-memresume.test
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/bin/bash
|
||||
# C2 (memory branch): a resume whose 206 lies text/html with a matching
|
||||
# INT64_MAX Content-Length must not overflow the resume buffer-size add
|
||||
# (UBSan aborts on the pre-fix binary at alloc_mem += totalsize). Pass 1 leaves
|
||||
# a partial + temp-ref; pass 2 resumes and must reject and refetch the whole
|
||||
# file. Teeth are UBSan-only: a normal build wraps to the same reject+restart.
|
||||
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_crangemem.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"
|
||||
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}/crange206mem/index.html" >"${tmpdir}/log1" 2>&1 &
|
||||
crawlpid=$!
|
||||
for _ in $(seq 1 300); do
|
||||
test -n "$(find "$refdir" -name '*.ref' 2>/dev/null)" && break
|
||||
kill -0 "$crawlpid" 2>/dev/null || break
|
||||
sleep 0.1
|
||||
done
|
||||
sleep 0.3
|
||||
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 the resume"
|
||||
exit 1
|
||||
}
|
||||
echo "OK (temp-ref present)"
|
||||
|
||||
# --- pass 2: --continue -> resume -> hostile INT64_MAX Content-Range ----------
|
||||
printf '[pass 2: hostile 206, no overflow, refetch] ..\t'
|
||||
httrack "${common[@]}" --continue "${base}/crange206mem/index.html" >"${tmpdir}/log2" 2>&1
|
||||
echo "OK (terminated)"
|
||||
|
||||
blob=$(find "$out" -name blob.bin 2>/dev/null | head -1)
|
||||
full=6008 # len(CRANGE206_BODY) = len("CR206DAT") + 6000
|
||||
|
||||
printf '[file recovered whole] ..\t'
|
||||
test -s "$blob" || {
|
||||
echo "FAIL: blob.bin missing after the hostile 206"
|
||||
exit 1
|
||||
}
|
||||
got=$(wc -c <"$blob")
|
||||
test "$got" -eq "$full" || {
|
||||
echo "FAIL: blob.bin is ${got} bytes, expected ${full}"
|
||||
exit 1
|
||||
}
|
||||
echo "OK (${got} bytes)"
|
||||
34
tests/49_local-cookiewall.test
Executable file
34
tests/49_local-cookiewall.test
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Cookie-wall self-redirect (#15): httrack replays a Set-Cookie to mirror the
|
||||
# real page, gives up when nothing changes, and stays bounded when it never does.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${top_srcdir:=..}"
|
||||
|
||||
# Unknown-ext (wall.php) and known-ext (wall.html): the wall sets the cookie once,
|
||||
# then serves real content; httrack must replay it and mirror that content.
|
||||
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 \
|
||||
--found 'cookiewall/wall.html' \
|
||||
--file-matches 'cookiewall/wall.html' 'REAL-CONTENT-BEHIND-COOKIE-WALL' \
|
||||
httrack 'BASEURL/cookiewall/index.html'
|
||||
|
||||
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 \
|
||||
--found 'cookiewall2/wall.html' \
|
||||
--file-matches 'cookiewall2/wall.html' 'REAL-CONTENT-BEHIND-COOKIE-WALL' \
|
||||
httrack 'BASEURL/cookiewall2/index.html'
|
||||
|
||||
# No Set-Cookie: the jar never changes, so httrack must give up at once (retry
|
||||
# stays gated) and not mirror the wall. A dropped gate would skip this log line.
|
||||
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 \
|
||||
--not-found 'cookiewall3/wall.html' \
|
||||
--log-found 'loop to same filename' \
|
||||
httrack 'BASEURL/cookiewall3/index.html'
|
||||
|
||||
# Ever-changing cookie never satisfies the wall: httrack must stop at the retry
|
||||
# cap (bounded, no early give-up) and not mirror it.
|
||||
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 \
|
||||
--not-found 'cookiewall4/wall.html' \
|
||||
--log-not-found 'loop to same filename' \
|
||||
httrack 'BASEURL/cookiewall4/index.html'
|
||||
@@ -30,6 +30,7 @@ TESTS = \
|
||||
01_engine-cmdline.test \
|
||||
01_engine-cookies.test \
|
||||
01_engine-copyopt.test \
|
||||
01_engine-crange.test \
|
||||
01_engine-dns.test \
|
||||
01_engine-doitlog.test \
|
||||
01_engine-entities.test \
|
||||
@@ -65,6 +66,7 @@ TESTS = \
|
||||
01_engine-urlhack.test \
|
||||
01_engine-unescape-bounds.test \
|
||||
01_engine-useragent.test \
|
||||
01_engine-xfread.test \
|
||||
01_zlib-acceptencoding.test \
|
||||
01_zlib-cache.test \
|
||||
01_zlib-cache-corrupt.test \
|
||||
@@ -110,6 +112,14 @@ TESTS = \
|
||||
38_local-update-304.test \
|
||||
39_local-delayed-cancel.test \
|
||||
40_local-why.test \
|
||||
41_local-utf8-link.test
|
||||
41_local-utf8-link.test \
|
||||
42_local-maxsize-slow.test \
|
||||
43_local-update-truncate.test \
|
||||
44_local-update-errormask.test \
|
||||
45_local-maxsize-recv.test \
|
||||
46_local-update-304-resume.test \
|
||||
47_local-crange-overflow.test \
|
||||
48_local-crange-memresume.test \
|
||||
49_local-cookiewall.test
|
||||
|
||||
CLEANFILES = check-network_sh.cache
|
||||
|
||||
@@ -105,7 +105,7 @@ function start-crawl {
|
||||
log="${tmp}/log"
|
||||
debug starting httrack -O "${tmp}" "${moreargs[@]}" "${@:pos}"
|
||||
info "running httrack ${*:pos}"
|
||||
httrack -O "${tmp}" --user-agent="httrack $ver ut ($(uname -omrs))" "${moreargs[@]}" "${@:pos}" >"${log}" 2>&1 &
|
||||
httrack -O "${tmp}" --user-agent="httrack $ver ut ($(uname -mrs))" "${moreargs[@]}" "${@:pos}" >"${log}" 2>&1 &
|
||||
crawlpid="$!"
|
||||
debug "started cralwer on pid $crawlpid"
|
||||
wait "$crawlpid"
|
||||
|
||||
@@ -13,15 +13,21 @@
|
||||
#
|
||||
# Usage:
|
||||
# bash local-crawl.sh [--tls] [--root DIR] [--cookie NAME=VALUE ...] \
|
||||
# --errors N --files N --found PATH ... --directory PATH ... \
|
||||
# [--rerun-args 'ARGS'] \
|
||||
# --errors N --errors-content N --files N --found PATH ... --directory PATH ... \
|
||||
# --log-found REGEX ... --log-not-found REGEX ... \
|
||||
# --file-matches PATH REGEX ... --file-not-matches PATH REGEX ... \
|
||||
# --max-mirror-bytes N \
|
||||
# --file-min-bytes PATH N --max-mirror-bytes N \
|
||||
# httrack BASEURL/some/path [httrack-args...]
|
||||
# --errors counts every "Error:" log line; --errors-content drops transient
|
||||
# network failures (codes -2..-6) that flake on busy loopback under -c8.
|
||||
# --log-found/--log-not-found grep (ERE) the crawl's hts-log.txt.
|
||||
# --max/--min-mirror-bytes bound the mirrored content bytes (host root).
|
||||
# --file-matches/--file-not-matches grep (ERE) a mirrored file (PATH under the
|
||||
# host root), to assert rewritten link/content survived the crawl.
|
||||
# --file-min-bytes asserts a mirrored file (PATH) is at least N bytes.
|
||||
# --rerun-args runs a second pass (same server and mirror dir) with the given
|
||||
# extra httrack args appended, e.g. an --update run under a cap.
|
||||
# --cookie writes a Netscape cookies.txt (scoped to the discovered host:port,
|
||||
# which the ephemeral port forces into the cookie domain) and passes it to
|
||||
# httrack via --cookies-file, to exercise preloaded cookies.
|
||||
@@ -39,6 +45,7 @@ key="${testdir}/server.key"
|
||||
tls=
|
||||
verbose=
|
||||
rerun=
|
||||
rerun_args=
|
||||
rerun_dead=
|
||||
tmpdir=
|
||||
serverpid=
|
||||
@@ -122,7 +129,11 @@ while test "$pos" -lt "$nargs"; do
|
||||
pos=$((pos + 1))
|
||||
cookies+=("${args[$pos]}")
|
||||
;;
|
||||
--errors | --files)
|
||||
--rerun-args)
|
||||
pos=$((pos + 1))
|
||||
rerun_args="${args[$pos]}"
|
||||
;;
|
||||
--errors | --errors-content | --files)
|
||||
audit+=("${args[$pos]}" "${args[$((pos + 1))]}")
|
||||
pos=$((pos + 1))
|
||||
;;
|
||||
@@ -130,7 +141,7 @@ while test "$pos" -lt "$nargs"; do
|
||||
audit+=("${args[$pos]}" "${args[$((pos + 1))]}")
|
||||
pos=$((pos + 1))
|
||||
;;
|
||||
--file-matches | --file-not-matches)
|
||||
--file-matches | --file-not-matches | --file-min-bytes)
|
||||
audit+=("${args[$pos]}" "${args[$((pos + 1))]}" "${args[$((pos + 2))]}")
|
||||
pos=$((pos + 2))
|
||||
;;
|
||||
@@ -202,7 +213,7 @@ mkdir "$out" || die "could not create $out"
|
||||
declare -a moreargs=(--quiet --max-time=120 --timeout=30 --disable-security-limits --robots=0)
|
||||
log="${tmpdir}/log"
|
||||
info "running httrack ${hts[*]}"
|
||||
httrack -O "$out" --user-agent="httrack $ver local ($(uname -omrs))" "${moreargs[@]}" "${hts[@]}" >"$log" 2>&1 &
|
||||
httrack -O "$out" --user-agent="httrack $ver local ($(uname -mrs))" "${moreargs[@]}" "${hts[@]}" >"$log" 2>&1 &
|
||||
crawlpid=$!
|
||||
wait "$crawlpid"
|
||||
crawlres=$?
|
||||
@@ -219,7 +230,7 @@ grep -iE "^[0-9:]*[[:space:]]Error:" "${out}/hts-log.txt" >&2
|
||||
# --- optional second pass: re-mirror into the same dir (cache/update path) ----
|
||||
if test -n "$rerun"; then
|
||||
info "re-running httrack (update pass)"
|
||||
httrack -O "$out" --user-agent="httrack $ver local ($(uname -omrs))" \
|
||||
httrack -O "$out" --user-agent="httrack $ver local ($(uname -mrs))" \
|
||||
"${moreargs[@]}" "${hts[@]}" >"${log}.2" 2>&1 &
|
||||
crawlpid=$!
|
||||
wait "$crawlpid"
|
||||
@@ -241,6 +252,25 @@ if test -n "$rerun"; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- optional second pass with extra args (e.g. an --update run under a cap) ---
|
||||
# Same server and mirror dir as the first pass, so the second pass sees the
|
||||
# cache the first pass wrote. Used to exercise re-fetch/update behaviour.
|
||||
if test -n "$rerun_args"; then
|
||||
read -ra extra <<<"$rerun_args"
|
||||
info "re-running httrack with ${rerun_args}"
|
||||
httrack -O "$out" --user-agent="httrack $ver local ($(uname -mrs))" \
|
||||
"${moreargs[@]}" "${hts[@]}" "${extra[@]}" >"${log}.2" 2>&1 &
|
||||
crawlpid=$!
|
||||
wait "$crawlpid"
|
||||
crawlres=$?
|
||||
crawlpid=
|
||||
test "$crawlres" -eq 0 || ! result "second pass exited $crawlres" || {
|
||||
cat "${log}.2" >&2
|
||||
exit 1
|
||||
}
|
||||
result "OK (second pass)"
|
||||
fi
|
||||
|
||||
# --- optional dead pass: server stopped, the cache must survive the rollback --
|
||||
if test -n "$rerun_dead"; then
|
||||
zip="${out}/hts-cache/new.zip"
|
||||
@@ -251,7 +281,7 @@ if test -n "$rerun_dead"; then
|
||||
wait "$serverpid" 2>/dev/null
|
||||
serverpid=
|
||||
info "re-running httrack against the stopped server"
|
||||
httrack -O "$out" --user-agent="httrack $ver local ($(uname -omrs))" \
|
||||
httrack -O "$out" --user-agent="httrack $ver local ($(uname -mrs))" \
|
||||
"${moreargs[@]}" "${hts[@]}" >"${log}.dead" 2>&1 &
|
||||
crawlpid=$!
|
||||
wait "$crawlpid" || true
|
||||
@@ -306,6 +336,14 @@ while test "$i" -lt "${#audit[@]}"; do
|
||||
assert_equals "checking errors" "${audit[$i]}" \
|
||||
"$(grep -iEc "^[0-9:]*[[:space:]]Error:" "${out}/hts-log.txt")"
|
||||
;;
|
||||
--errors-content)
|
||||
i=$((i + 1))
|
||||
total=$(grep -icE "^[0-9:]*[[:space:]]Error:" "${out}/hts-log.txt")
|
||||
# transient network failures (statuscode -2..-6) flake on busy loopback;
|
||||
# the code parens are followed by " at link" or " after N retries at link"
|
||||
transient=$(grep -cE '\(-[2-6]\) (at link|after )' "${out}/hts-log.txt" || true)
|
||||
assert_equals "checking content errors" "${audit[$i]}" "$((total - transient))"
|
||||
;;
|
||||
--files)
|
||||
i=$((i + 1))
|
||||
nFiles=$(grep -E "^HTTrack Website Copier/[^ ]* mirror complete in " "${out}/hts-log.txt" |
|
||||
@@ -388,6 +426,16 @@ while test "$i" -lt "${#audit[@]}"; do
|
||||
exit 1
|
||||
else result "OK"; fi
|
||||
;;
|
||||
--file-min-bytes)
|
||||
path="${audit[$((i + 1))]}"
|
||||
i=$((i + 2))
|
||||
sz=$(wc -c <"${hostroot}/${path}" 2>/dev/null | tr -d '[:space:]')
|
||||
info "checking ${path} size ${sz:-0} >= ${audit[$i]} bytes"
|
||||
if test -n "$sz" && test "$sz" -ge "${audit[$i]}"; then result "OK"; else
|
||||
result "file too small (or missing)"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
@@ -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
|
||||
@@ -855,6 +905,94 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
if self.command != "HEAD":
|
||||
self.wfile.write(part)
|
||||
|
||||
# C2: a resume answered with a 206 whose Content-Range end is INT64_MAX would
|
||||
# sign-overflow the crange+1 range check (UBSan abort). Stall first (partial +
|
||||
# ref), then answer the resume Range with that hostile 206; httrack must reject
|
||||
# the range and refetch, never overflow.
|
||||
CRANGE206_BODY = b"CR206DAT" + bytes((i * 5 + 1) % 256 for i in range(6000))
|
||||
_crange206_started = False
|
||||
|
||||
def route_crange206_index(self):
|
||||
self.send_html('\t<a href="blob.bin">blob</a>')
|
||||
|
||||
def route_crange206(self):
|
||||
rng = self.headers.get("Range")
|
||||
if not Handler._crange206_started:
|
||||
Handler._crange206_started = True
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/octet-stream")
|
||||
self.send_header("Content-Length", str(len(self.CRANGE206_BODY)))
|
||||
self.send_header("Accept-Ranges", "bytes")
|
||||
self.end_headers()
|
||||
if self.command != "HEAD":
|
||||
self.wfile.write(self.CRANGE206_BODY[:3000])
|
||||
self.wfile.flush()
|
||||
try:
|
||||
while True:
|
||||
time.sleep(3600)
|
||||
except OSError:
|
||||
pass
|
||||
return
|
||||
if rng is not None: # resume: hostile 206, Content-Range end = INT64_MAX
|
||||
self.send_response(206, "Partial Content")
|
||||
self.send_header("Content-Type", "application/octet-stream")
|
||||
self.send_header("Content-Length", str(len(self.CRANGE206_BODY)))
|
||||
self.send_header("Content-Range", "bytes 0-9223372036854775807/1")
|
||||
self.end_headers()
|
||||
if self.command != "HEAD":
|
||||
self.wfile.write(self.CRANGE206_BODY)
|
||||
return
|
||||
# range-less refetch after the bad range is rejected: whole file.
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/octet-stream")
|
||||
self.send_header("Content-Length", str(len(self.CRANGE206_BODY)))
|
||||
self.end_headers()
|
||||
if self.command != "HEAD":
|
||||
self.wfile.write(self.CRANGE206_BODY)
|
||||
|
||||
# C2 memory branch: a resume answered with a 206 that lies text/html (so the
|
||||
# resume buffers in memory) plus a matching INT64_MAX Content-Length would
|
||||
# overflow the buffer-size add. Stall first, then send that hostile 206.
|
||||
_crange206mem_started = False
|
||||
|
||||
def route_crange206mem_index(self):
|
||||
self.send_html('\t<a href="blob.bin">blob</a>')
|
||||
|
||||
def route_crange206mem(self):
|
||||
rng = self.headers.get("Range")
|
||||
if not Handler._crange206mem_started:
|
||||
Handler._crange206mem_started = True
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/octet-stream")
|
||||
self.send_header("Content-Length", str(len(self.CRANGE206_BODY)))
|
||||
self.send_header("Accept-Ranges", "bytes")
|
||||
self.end_headers()
|
||||
if self.command != "HEAD":
|
||||
self.wfile.write(self.CRANGE206_BODY[:3000])
|
||||
self.wfile.flush()
|
||||
try:
|
||||
while True:
|
||||
time.sleep(3600)
|
||||
except OSError:
|
||||
pass
|
||||
return
|
||||
if rng is not None: # resume: text/html + matching INT64_MAX Content-Length
|
||||
self.send_response(206, "Partial Content")
|
||||
self.send_header("Content-Type", "text/html")
|
||||
self.send_header("Content-Length", "9223372036854775807")
|
||||
self.send_header(
|
||||
"Content-Range", "bytes 0-9223372036854775806/9223372036854775807"
|
||||
)
|
||||
self.end_headers()
|
||||
return # the overflow is computed before any body read
|
||||
# range-less refetch after the resume is rejected: whole file.
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/octet-stream")
|
||||
self.send_header("Content-Length", str(len(self.CRANGE206_BODY)))
|
||||
self.end_headers()
|
||||
if self.command != "HEAD":
|
||||
self.wfile.write(self.CRANGE206_BODY)
|
||||
|
||||
# error pages / 0-byte files (#17): -o0 ("no error pages") must keep 4xx/5xx
|
||||
# bodies off disk; a genuine 0-byte 200 is a valid file and stays.
|
||||
def route_errpage_index(self):
|
||||
@@ -934,6 +1072,33 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
def route_mini304_page(self):
|
||||
self.big_send(b"<html><body>tiny cacheable page</body></html>\n", "text/html")
|
||||
|
||||
# --- /errmask/: issue #176 — a page that 200'd on the first crawl but 403s
|
||||
# on the update fetch must keep its good copy, not be overwritten nor purged.
|
||||
ERRMASK_GOOD = b"KEEP" + b"." * 1020 # 1024 B distinctive non-HTML body
|
||||
ERRMASK_ERR = b"<html><body>error 403</body></html>\n"
|
||||
|
||||
def route_errmask_index(self):
|
||||
self.send_html('\t<a href="keep.dat">keep</a>\n')
|
||||
|
||||
def route_errmask_keep(self):
|
||||
# First crawl (no validator) gets the 1024 B body + Last-Modified; the
|
||||
# update sends a conditional and gets a 403 error page.
|
||||
if self.headers.get("If-Modified-Since") or self.headers.get("If-None-Match"):
|
||||
self.send_response(403, "Forbidden")
|
||||
self.send_header("Content-Type", "text/html")
|
||||
self.send_header("Content-Length", str(len(self.ERRMASK_ERR)))
|
||||
self.end_headers()
|
||||
if self.command != "HEAD":
|
||||
self.wfile.write(self.ERRMASK_ERR)
|
||||
return
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/octet-stream")
|
||||
self.send_header("Last-Modified", BIG_LASTMOD)
|
||||
self.send_header("Content-Length", str(len(self.ERRMASK_GOOD)))
|
||||
self.end_headers()
|
||||
if self.command != "HEAD":
|
||||
self.wfile.write(self.ERRMASK_GOOD)
|
||||
|
||||
# --- delayed-type degenerate paths (issues #5/#107) --------------------
|
||||
def route_delayed_index(self):
|
||||
self.send_html(
|
||||
@@ -978,6 +1143,56 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
def route_delayed_empty(self):
|
||||
self.send_raw(b"", "text/html") # 200 + Content-Length: 0
|
||||
|
||||
# --- /cookiewall/ (#15): a self-redirect that only sets a cookie is a
|
||||
# consent wall; httrack must replay the cookie and fetch the real page.
|
||||
WALL_MARK = b"REAL-CONTENT-BEHIND-COOKIE-WALL"
|
||||
|
||||
def route_cookiewall_index(self):
|
||||
self.send_html('\t<a href="wall.php">wall</a>')
|
||||
|
||||
def route_cookiewall_wall(self):
|
||||
self._cookiewall_reply("wall.php")
|
||||
|
||||
# Known-extension twin: .html so the type is not delayed-resolved.
|
||||
def route_cookiewall2_index(self):
|
||||
self.send_html('\t<a href="wall.html">wall</a>')
|
||||
|
||||
def route_cookiewall2_wall(self):
|
||||
self._cookiewall_reply("wall.html")
|
||||
|
||||
def _cookiewall_reply(self, location):
|
||||
if self.request_cookies().get("gate") == "1":
|
||||
self.send_raw(
|
||||
b"<html><body>" + self.WALL_MARK + b"</body></html>\n", "text/html"
|
||||
)
|
||||
else:
|
||||
self._wall_redirect(location, "gate=1; Path=/")
|
||||
|
||||
# No-cookie self-redirect: the jar never changes, so httrack must give up at
|
||||
# once rather than re-fetch (proves the cookie-wall retry stays gated on #15).
|
||||
def route_cookiewall3_index(self):
|
||||
self.send_html('\t<a href="wall.php">wall</a>')
|
||||
|
||||
def route_cookiewall3_wall(self):
|
||||
self._wall_redirect("wall.php", None)
|
||||
|
||||
# Ever-changing cookie: every hit sets a fresh value, so the jar keeps
|
||||
# changing; httrack must stop at the loops<7 cap, not spin forever.
|
||||
def route_cookiewall4_index(self):
|
||||
self.send_html('\t<a href="wall.php">wall</a>')
|
||||
|
||||
def route_cookiewall4_wall(self):
|
||||
nonce = int(self.request_cookies().get("gate", "0")) + 1
|
||||
self._wall_redirect("wall.php", f"gate={nonce}; Path=/")
|
||||
|
||||
def _wall_redirect(self, location, set_cookie):
|
||||
self.send_response(302, "Found")
|
||||
self.send_header("Location", location)
|
||||
if set_cookie is not None:
|
||||
self.send_header("Set-Cookie", set_cookie)
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
|
||||
# -E time-limit (#481): pages that trickle far longer than any -E budget,
|
||||
# so only an engine-side abort can end the crawl.
|
||||
TRICKLE_SECONDS = 60
|
||||
@@ -1035,6 +1250,64 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
def route_bigfile(self):
|
||||
self.send_raw(b"x" * self.BIGFILE_BYTES, "application/octet-stream")
|
||||
|
||||
# -M under a slow server (#77): p0 is a fast 640KB file that alone overruns
|
||||
# -M; p1..p3 trickle for a minute. The cap must abort those in-flight
|
||||
# transfers, not wait them out.
|
||||
def route_bigtrickle_index(self):
|
||||
self.send_html(
|
||||
"".join('\t<a href="p%d.bin">p%d</a>\n' % (i, i) for i in range(4))
|
||||
)
|
||||
|
||||
# -M hard-abort must not destroy an already-complete file (#77 follow-up).
|
||||
# "fast.bin" alone overruns -M and completes; "slow.bin" transfers fully on
|
||||
# its first fetch (initial mirror) but stalls on every later fetch (the
|
||||
# --update re-fetch), so the -M abort tears it down mid-body. An engine that
|
||||
# truncates the good local copy on the aborted re-fetch loses data.
|
||||
slow_seen = 0
|
||||
|
||||
def route_bigtrunc_index(self):
|
||||
self.send_html('\t<a href="fast.bin">fast</a>\n\t<a href="slow.bin">slow</a>\n')
|
||||
|
||||
def route_bigtrunc_slow(self):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/octet-stream")
|
||||
self.send_header("Content-Length", str(self.BIGFILE_BYTES))
|
||||
self.end_headers()
|
||||
if self.command == "HEAD":
|
||||
return
|
||||
# Count body fetches only, so a stray HEAD can't shift which pass stalls.
|
||||
Handler.slow_seen += 1
|
||||
first = Handler.slow_seen == 1
|
||||
try:
|
||||
if first:
|
||||
self.wfile.write(b"x" * self.BIGFILE_BYTES)
|
||||
self.wfile.flush()
|
||||
else:
|
||||
self.wfile.write(b"x" * 4096)
|
||||
self.wfile.flush()
|
||||
for _ in range(120):
|
||||
self.wfile.write(b"x")
|
||||
self.wfile.flush()
|
||||
time.sleep(1.0)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# -M received-volume cap (#520): links to large 404 bodies. httrack receives
|
||||
# each (HTS_TOTAL_RECV climbs) but saves none, so saved stays far below -M.
|
||||
def route_maxrecv_index(self):
|
||||
self.send_html(
|
||||
"".join('\t<a href="r%d.bin">r%d</a>\n' % (i, i) for i in range(16))
|
||||
)
|
||||
|
||||
def route_maxrecv_404(self):
|
||||
body = b"x" * self.BIGFILE_BYTES
|
||||
self.send_response(404, "Not Found")
|
||||
self.send_header("Content-Type", "application/octet-stream")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
if self.command != "HEAD":
|
||||
self.wfile.write(body)
|
||||
|
||||
ROUTES = {
|
||||
"/cookies/entrance.php": route_entrance,
|
||||
"/cookies/second.php": route_second,
|
||||
@@ -1064,9 +1337,15 @@ 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,
|
||||
"/crange206/index.html": route_crange206_index,
|
||||
"/crange206/blob.bin": route_crange206,
|
||||
"/crange206mem/index.html": route_crange206mem_index,
|
||||
"/crange206mem/blob.bin": route_crange206mem,
|
||||
"/size/index.html": route_size_index,
|
||||
"/size/oversize.bin": route_size_oversize,
|
||||
"/errpage/index.html": route_errpage_index,
|
||||
@@ -1107,6 +1386,14 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
"/bigfiles/p5.bin": route_bigfile,
|
||||
"/bigfiles/p6.bin": route_bigfile,
|
||||
"/bigfiles/p7.bin": route_bigfile,
|
||||
"/bigtrickle/index.html": route_bigtrickle_index,
|
||||
"/bigtrickle/p0.bin": route_bigfile,
|
||||
"/bigtrickle/p1.bin": route_trickle_page,
|
||||
"/bigtrickle/p2.bin": route_trickle_page,
|
||||
"/bigtrickle/p3.bin": route_trickle_page,
|
||||
"/bigtrunc/index.html": route_bigtrunc_index,
|
||||
"/bigtrunc/fast.bin": route_bigfile,
|
||||
"/bigtrunc/slow.bin": route_bigtrunc_slow,
|
||||
"/delayed/noloc.php": route_delayed_noloc,
|
||||
"/delayed/selfloop.php": route_delayed_selfloop,
|
||||
"/delayed/redir.php": route_delayed_redir,
|
||||
@@ -1122,11 +1409,38 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
"/delayed/chain7.php": route_delayed_chain,
|
||||
"/delayed/chain8.php": route_delayed_chain,
|
||||
"/delayed/chain9.php": route_delayed_chain,
|
||||
"/cookiewall/index.html": route_cookiewall_index,
|
||||
"/cookiewall/wall.php": route_cookiewall_wall,
|
||||
"/cookiewall2/index.html": route_cookiewall2_index,
|
||||
"/cookiewall2/wall.html": route_cookiewall2_wall,
|
||||
"/cookiewall3/index.html": route_cookiewall3_index,
|
||||
"/cookiewall3/wall.php": route_cookiewall3_wall,
|
||||
"/cookiewall4/index.html": route_cookiewall4_index,
|
||||
"/cookiewall4/wall.php": route_cookiewall4_wall,
|
||||
"/redir/index.html": route_redir_index,
|
||||
"/redir/go.php": route_redir_go,
|
||||
"/redir/target.html": route_redir_target,
|
||||
"/mini304/index.html": route_mini304_index,
|
||||
"/mini304/page.html": route_mini304_page,
|
||||
"/errmask/index.html": route_errmask_index,
|
||||
"/errmask/keep.dat": route_errmask_keep,
|
||||
"/maxrecv/index.html": route_maxrecv_index,
|
||||
"/maxrecv/r0.bin": route_maxrecv_404,
|
||||
"/maxrecv/r1.bin": route_maxrecv_404,
|
||||
"/maxrecv/r2.bin": route_maxrecv_404,
|
||||
"/maxrecv/r3.bin": route_maxrecv_404,
|
||||
"/maxrecv/r4.bin": route_maxrecv_404,
|
||||
"/maxrecv/r5.bin": route_maxrecv_404,
|
||||
"/maxrecv/r6.bin": route_maxrecv_404,
|
||||
"/maxrecv/r7.bin": route_maxrecv_404,
|
||||
"/maxrecv/r8.bin": route_maxrecv_404,
|
||||
"/maxrecv/r9.bin": route_maxrecv_404,
|
||||
"/maxrecv/r10.bin": route_maxrecv_404,
|
||||
"/maxrecv/r11.bin": route_maxrecv_404,
|
||||
"/maxrecv/r12.bin": route_maxrecv_404,
|
||||
"/maxrecv/r13.bin": route_maxrecv_404,
|
||||
"/maxrecv/r14.bin": route_maxrecv_404,
|
||||
"/maxrecv/r15.bin": route_maxrecv_404,
|
||||
}
|
||||
|
||||
# --- /big/ seeded pseudo-site ------------------------------------------
|
||||
@@ -1331,7 +1645,12 @@ def main():
|
||||
def factory(*a, **kw):
|
||||
return Handler(*a, directory=root, **kw)
|
||||
|
||||
httpd = ThreadingHTTPServer((args.bind, 0), factory)
|
||||
# macOS/BSD drop SYNs when the listen backlog overflows (Linux is lenient);
|
||||
# raise it from Python's default 5 so a busy -c8 crawl can't lose fetches.
|
||||
class BacklogHTTPServer(ThreadingHTTPServer):
|
||||
request_queue_size = 128
|
||||
|
||||
httpd = BacklogHTTPServer((args.bind, 0), factory)
|
||||
|
||||
if args.tls:
|
||||
import ssl
|
||||
|
||||
Reference in New Issue
Block a user