Compare commits

...

11 Commits

Author SHA1 Message Date
Xavier Roche
ee550c3972 Bound test 19's dead-address connects so it is fast on macOS
19_local-connect-fallback pins dead loopback IPs (127.0.0.2/.3) before the live
one to exercise address fallback. On Linux those refuse instantly (all of
127.0.0.0/8 is loopback); macOS has only 127.0.0.1, so the connects block to
--timeout, and default retries replay the whole crawl, so the exhaustion case
took ~2 minutes. It passed but dominated the macOS suite once the rest runs in
parallel. Drop --timeout to 5s (bounds the last candidate's stall; the earlier
ones already fall back at min(timeout,10)) and set --retries=0 (no ×3 replay).
Linux is unchanged: the refused connects never reach the timeout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-11 16:54:25 +02:00
Xavier Roche
145ea4769f Make test 31 fail loudly when the java plugin doesn't load (#529)
31_local-javaclass verifies the java plugin parses a .class constant pool
by asserting a resource named only inside Foo.class gets crawled. When the
plugin fails to load, that assertion just reports hello.gif "not found",
with no hint the plugin was the cause -- which is why an intermittent
local failure sat uninvestigated.

The failure mode is a shadowing system libhtsjava: on a box with httrack
installed, if the soname httrack dlopens is absent from the fresh build
but present in /usr/lib, the loader picks up the system plugin (a
mismatched, non-instrumented build) and it silently parses nothing. It
looked ASan-specific only because that was the build in use; clean CI
runners have no system plugin, so they never hit it. #475 aligned the
dlopen name with the build's own soname, closing the gap.

Assert the plugin's launched-banner suffix ("+libhtsjava") first, so a
load failure trips there with a clear message instead of the opaque
hello.gif miss. The "+libhtsjava" prefix survives future soname bumps.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 16:33:45 +02:00
Xavier Roche
c5da5a2f2e Stop test 36 flaking on macOS when loopback drops connections (#527)
* Stop test 36 flaking on macOS when loopback drops connections

The 36_local-bigcrawl crawl runs -c8 over 361 files and asserted its
error count exactly (--errors 4, the planted 404/410/500/gztrunc). On the
macOS CI runner the busy loopback intermittently resets a few in-flight
connections, so the count came back 7 and the job failed while Linux
passed.

Add a --errors-content harness assertion that counts every "Error:" line
minus the transient-network family (statuscodes -2..-7:
timeout/connect/reset), and switch test 36 to it. The four planted
content errors (404/410/500 and the -1 decompression failure) still count
exactly, so a real regression that adds a content/HTTP error still fails;
only loopback flakiness is tolerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Tighten --errors-content: drop -7 from transient, match retried errors

Review follow-up. Two fixes to the transient-error filter:

- Narrow the excluded band from -2..-7 to -2..-6. STATUSCODE_TOO_BIG (-7)
  is a deterministic size-cap rejection, not a network transient, so it
  must count as a real error; excluding it could mask a -M regression in
  a future test that reuses this option.

- Also match the "after N retries" error format. htsparse.c logs
  "<msg>" (<code>) after N retries at link ... when retries are enabled,
  which the old "(-code) at link" regex missed. Test 36 forces
  --retries=0 so only the immediate format appears there, but the option
  is a general harness primitive and should hold under default retries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 15:25:59 +02:00
Xavier Roche
6eaefe6233 Type LLint/TStamp as int64_t and drop the per-platform boilerplate (#526)
* Type LLint/TStamp as int64_t, drop the per-platform boilerplate

The wide-integer typedefs picked long/long long/__int64 per platform and
carried a matching printf-format ladder plus the HTS_LONGLONG capability
macro. That is what let x32 pick a 32-bit long for a "64-bit" LLint (#524).

<stdint.h>/<inttypes.h> answer both questions directly: int64_t is exactly
64-bit signed with defined wrap, and "%" PRId64 is its conversion. LLint
stays signed because -1 is a size/range sentinel across the engine (a
uint64_t would silently break every "< 0" check). The names LLint, TStamp,
and the LLintP macro are kept, so the ~70 call sites are untouched.

HTS_LONGLONG is now dead (a 64-bit type is a hard C99 dependency here:
md5.h includes <stdint.h> and the installed httrack-library.h includes
<inttypes.h>), so its detection blocks and htslib.c's #ifdef around the
GiB/TiB/PiB formatting go too; those branches are always valid now.

No ABI change: int64_t is `long` on LP64, so httrackp/htsblk stay
byte-identical and the exported symbol set is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Tighten the LLint typedef comments

Two one-line blocks instead of a four-line header; drop the change-narration
("no per-platform ladder"). Keep the load-bearing note that LLintP carries its
own '%'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 15:09:54 +02:00
Xavier Roche
33249be5a0 Assert kept content in the update-error test, not just size (#176 follow-up) (#525)
44_local-update-errormask checked only keep.dat's size (>= 1024). The good
body carries a distinctive KEEP marker that nothing asserted, so a same-size
wrong-content overwrite would still pass. Also assert the content matches
^KEEP and lacks the 403 error body, so the test discriminates the fix from
an equal-size clobber the size check alone misses.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 14:49:01 +02:00
Xavier Roche
167ede32c0 Keep the good local copy when an update fetch returns an HTTP error (#176) (#523)
On --update, a URL that returned 200 on the first crawl but now answers a
transient error (403/404/5xx) had its good local copy destroyed: the error
body overwrote it (errpage is on by default) and/or the delete_old purge
removed it. The guard that masks such an error as a 304 to keep the cached
copy was gated on !opt->delete_old, but delete_old is on by default, so it
never ran for a normal update.

Drop that outer gate so the masking runs by default, but restrict it to the
complete-cached-copy case (range_req_size == 0): an error on a resume/range
fetch must still fall through, else a stale-partial 416 would be masked to
304 and the partial never re-fetched (regressing #206). The masked error
routes through the not-modified handler, which reloads and re-registers the
cached file, blocking both the overwrite and the purge. Unlinked-page purging
is unaffected: it targets pages not fetched this run, not a fetched page that
errored.

Test 44_local-update-errormask drives it over a local server whose keep.dat
200s on the first crawl and 403s on the conditional update fetch.

Closes #176

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 14:30:43 +02:00
Xavier Roche
c8b7c63c9b Keep LLint 64-bit on the x32 ABI (#524)
The wide-integer typedef keyed on __x86_64__, which the x32 ABI also
defines even though its long is 32-bit. That silently made LLint -- the
signed 64-bit type for byte counts and file sizes -- 32-bit on x32, so
any value past INT_MAX overflowed (a >2GB size wrapped negative and the
cache dropped the entry). Exclude __ILP32__ so x32 takes the long-long
path, plus a compile-time guard that trips wherever 64-bit support is
claimed but LLint isn't actually 64-bit.

Surfaced by the cache selftests on Debian x32: cache-writefail (oversize)
and cache-corrupt both exercise >2GB entries and failed.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 14:21:27 +02:00
Xavier Roche
04f37b2094 Don't destroy already-complete files when -M/-E aborts an --update (#522)
An --update run stopped by the -M size cap (or -E time cap) could wipe out
files that were already fully mirrored, via two paths:

  - An incomplete re-fetch opens the real file "wb" (truncating the good copy)
    before any body byte arrives; the hard-abort then leaves it truncated.
  - A file that fully arrives is committed, but the aborted slot then reaches
    the main-loop save with statuscode <= 0 and size 0, and filesave() writes a
    zero-byte file over it -- leaving the file empty on disk while the cache
    still records it 200 OK, so a later --update never re-fetches it (permanent
    loss).

For the first path, a re-fetch over an existing file now moves the good copy
aside before truncating; back_finalize commits the fresh file on a full body and
restores the previous copy on an aborted/incomplete transfer. url_sav is still
written the normal way, so the file list, update purge and cache bookkeeping are
unchanged. For the second, an empty body only overwrites the file on a real
response (statuscode > 0), never on a failed transfer.

Adds 43_local-update-truncate.test with a stateful bigtrunc fixture that
mirrors, then re-fetches under --update -M and asserts both the completed and
the incomplete-refetch file keep their full size across the abort. The harness
gains --rerun-args (a second pass with extra args) and --file-min-bytes.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 11:20:29 +02:00
Xavier Roche
c07b19bc3d -M overall-size cap never bounds the overshoot under a slow server (#519)
* -M byte cap only smooth-stopped, never bounded the overshoot (#77)

The overall-size limit (-M / maxsite) requested a smooth stop once
stat_bytes crossed the cap but back_checkmirror always returned 1, so a
slow or throttling server let the in-flight transfers drain at its own
pace and the wait loops starved on them. On a bot-throttled archive the
mirror ran for over an hour and blew far past the cap. This is the size
counterpart of the -E fix in #482, which left the maxsite branch alone.

Give -M the same grace-then-abort escape hatch: once saved bytes overrun
the cap by maxsite/10, back_checkmirror returns 0 and the existing #482
abort path tears down the in-flight HTTP transfers (FTP slots stay with
their thread; real-named partials survive for --continue). The generic
abort log is reworded from "time limit" to "mirror limit" since it now
covers both quotas.

42_local-maxsize-slow.test crawls a server whose files trickle for a
minute under -M400000: the fixed engine stops in about 8 seconds, the
unfixed binary waits the full 60s.

The -M meter itself (saved bytes, which undercounts redirect/plugin-heavy
crawls vs. bytes actually received) is unchanged here; that is a separate
semantic question tracked as a follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Name the abort cause (size vs time) instead of a generic "mirror limit"

The #482 abort log and per-slot status were reworded to a generic "mirror
limit" once the size cap could also drive them, which dropped the cause. Add
an hts_mirror_limit enum and a back_mirror_limit() helper that reports which
cap overran its grace; back_checkmirror() now delegates its hard-stop decision
to it (removing the duplicated grace conditions), and the back_wait abort loop
uses it to log "size limit reached" / "time limit reached" and stamp the slot
"Mirror Size Limit" / "Mirror Time Out". This restores the precise wording -E
had before and gives the size path its own.

back_maxsize_grace keeps no floor, unlike the time grace: a size overrun
should abort promptly rather than let more bytes through. Documented inline.

Dropped the `--found bigtrickle/p0.bin` check from the test: the #482 hard
abort truncates a large file that is being re-fetched when the cap trips
(reproducible on master's -E path too), so p0.bin is legitimately 0 bytes on
many runs. The wall-clock bound is the discriminating assertion; the "giving
up" line fires for the unfixed engine as well.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 09:17:13 +02:00
Xavier Roche
04bbb489cf Drop the per-request pre-resolve that slowed crawls on non-default ports (#181) (#518)
back_solve() resolved the raw "host:port" authority once per backing
request through check_hostname_dns() — an uncached getaddrinfo whose
result was only logged (host_wait() is always synchronous now). The
connect path already strips the port and resolves through the DNS cache,
so this lookup was pure overhead. On resolvers that don't fail fast on
the malformed "host:port" name (search-domain expansion, DNS timeouts)
it added a per-request stall: the reported slowness. Mirrors on port 80,
or with the port omitted, avoided the bad name and stayed fast.

Remove back_solve() and its debug-only lookup; keep the synchronous
host_wait() gate. Add a DNS self-test pinning that the cache/connect
path resolves "host:port" as the bare host.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 00:13:40 +02:00
Xavier Roche
a24b1d3c9f Release 3.49.12 (#517)
Roll up 25 commits since 3.49.11 (July 5): security fixes in the
network-facing parsers (remote stack overflow and uninitialized read in
Content-Type/-Encoding handling, fuzzer-found over-reads in the filter,
URL and IDNA parsers, cache-index bounds, filter-pattern backtracking,
world-readable cookies.txt), crawl-correctness fixes (double-encoded UTF-8
links, gzip-mislabeled bodies, cache reconcile/corruption handling,
orphaned .delayed placeholders), the new --why filter diagnostic, and
build hardening (_FORTIFY_SOURCE, -fstack-protector-strong).

VERSION_INFO 3:3:0 -> 3:4:0: httrackp tail-appends why_url, no layout or
exported-signature break, so soname stays libhttrack.so.3 (revision bump).

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 21:28:23 +02:00
18 changed files with 410 additions and 211 deletions

View File

@@ -1,6 +1,6 @@
AC_PREREQ([2.71])
AC_INIT([httrack], [3.49.11], [roche+packaging@httrack.com], [httrack], [http://www.httrack.com/])
AC_INIT([httrack], [3.49.12], [roche+packaging@httrack.com], [httrack], [http://www.httrack.com/])
AC_COPYRIGHT([
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 1998-2015 Xavier Roche and other contributors
@@ -29,11 +29,11 @@ AC_CONFIG_SRCDIR(src/httrack.c)
AC_CONFIG_MACRO_DIR([m4])
AC_CONFIG_HEADERS(config.h)
AM_INIT_AUTOMAKE([subdir-objects])
# 3:3:0: 3.49.11 only adds enum values, macros and inline helpers to the
# installed headers (no struct layout or exported signature changed vs
# 3.49.10), so it stays soname .so.3; bump revision.
# 3:4:0: 3.49.12 tail-appends one field (why_url) to httrackp and otherwise only
# deletes dead comments; no struct layout or exported signature broke vs 3.49.11,
# so it stays soname .so.3; bump revision.
# (3:0:0 was the htsblk mime-buffer widening, the ABI break that moved .so.2 -> .so.3.)
VERSION_INFO="3:3:0"
VERSION_INFO="3:4:0"
AM_MAINTAINER_MODE
AC_USE_SYSTEM_EXTENSIONS

10
debian/changelog vendored
View File

@@ -1,3 +1,13 @@
httrack (3.49.12-1) unstable; urgency=medium
* New upstream release: security and crawl-correctness fixes (remote stack
overflow in Content-Type/-Encoding parsing, fuzzer-found parser over-reads,
world-readable cookies.txt, filter-pattern denial of service) plus a new
--why filter diagnostic; full list in history.txt.
* Build with _FORTIFY_SOURCE and -fstack-protector-strong.
-- Xavier Roche <xavier@debian.org> Fri, 10 Jul 2026 21:11:09 +0200
httrack (3.49.11-1) unstable; urgency=medium
* New upstream release: crawl correctness and security fixes (network-facing

View File

@@ -4,6 +4,23 @@ HTTrack Website Copier release history:
This file lists all changes and fixes that have been made for HTTrack
3.49-12
+ New: --why explains which filter rule accepts or rejects a given URL, then exits (#505)
+ Fixed: links carrying raw UTF-8 bytes were fetched double-encoded and 404'd (#516)
+ Fixed: an uncompressed body mislabeled as gzip no longer loses the page (#515)
+ Fixed: remote stack overflow and uninitialized read in Content-Type/-Encoding parsing (#506)
+ Fixed: several over-reads and a leak in the filter, URL and IDNA parsers (#499)
+ Fixed: bound the cache-index (.ndx) parser to its buffer (#507)
+ Fixed: catastrophic backtracking on '*'-heavy filter patterns (#513)
+ Fixed: cookies.txt was created world-readable (#511)
+ Fixed: a single corrupt cache entry no longer aborts the whole mirror (#494)
+ Fixed: cache-reconcile policy was broken for zip caches (#491, #493, #495)
+ Fixed: cancelling a crawl mid type-check no longer orphans .delayed placeholders (#496)
+ Fixed: detect URLs after the first inline script and in mid-tag attributes (#497)
+ Changed: build with _FORTIFY_SOURCE and -fstack-protector-strong (#504)
+ Changed: removed the pre-3.31 (.dat/.ndx) cache import (#512)
+ Changed: multiple internal hardening and build improvements (libFuzzer harnesses, CodeQL, dead-code removal)
3.49-11
+ New: parse robots.txt Allow rules and path wildcards per RFC 9309 (#452)
+ New: advertise deflate in Accept-Encoding and decode deflate responses (#450)

View File

@@ -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) {
@@ -3509,22 +3540,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
@@ -4145,38 +4174,43 @@ 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). */
static hts_mirror_limit back_mirror_limit(httrackp *opt) {
if (opt->maxsite > 0 && HTS_STAT.stat_bytes >= opt->maxsite &&
HTS_STAT.stat_bytes - 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.stat_bytes >= 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

View File

@@ -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

View File

@@ -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..) */

View File

@@ -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. */
{

View File

@@ -43,8 +43,8 @@ Please visit our Website: http://www.httrack.com
configure.ac, decoupled from these). VERSION is the display form, VERSIONID
the dotted numeric form, AFF_VERSION the short form shown in footers,
LIB_VERSION the data/cache format generation. */
#define HTTRACK_VERSION "3.49-11"
#define HTTRACK_VERSIONID "3.49.11"
#define HTTRACK_VERSION "3.49-12"
#define HTTRACK_VERSIONID "3.49.12"
#define HTTRACK_AFF_VERSION "3.x"
#define HTTRACK_LIB_VERSION "2.0"
@@ -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

View File

@@ -3038,12 +3038,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 +3051,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 +3068,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;

View File

@@ -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" || {

View File

@@ -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'

View File

@@ -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' \

View 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

View 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

View 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'

View File

@@ -110,6 +110,9 @@ 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
CLEANFILES = check-network_sh.cache

View File

@@ -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))
;;
@@ -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 -omrs))" \
"${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"
@@ -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

View File

@@ -934,6 +934,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(
@@ -1035,6 +1062,48 @@ 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
ROUTES = {
"/cookies/entrance.php": route_entrance,
"/cookies/second.php": route_second,
@@ -1107,6 +1176,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,
@@ -1127,6 +1204,8 @@ class Handler(SimpleHTTPRequestHandler):
"/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,
}
# --- /big/ seeded pseudo-site ------------------------------------------