Compare commits

..

7 Commits

Author SHA1 Message Date
Xavier Roche
9644516bee 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>
2026-07-11 08:55:29 +02:00
Xavier Roche
42c8c23aad -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>
2026-07-11 08:29:25 +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
Xavier Roche
1abf867333 Links with raw UTF-8 bytes are fetched double-encoded (404) (#516)
* Fix double-encoded requests for raw UTF-8 links (#180)

Links holding raw non-ASCII bytes were always converted from the page
charset to UTF-8, even when already UTF-8: with the iso-8859-1 fallback
(reached whenever the charset was declared via HTML5 <meta charset=..>,
which the meta scanner predates), each UTF-8 byte was re-encoded and the
mirror requested %c3%a7%c2%bb... instead of %e7%bb..., saving a 404.

Gate the conversion on a strict (RFC 3629) hts_isStringUTF8, and rewrite
hts_getCharsetFromMeta as a size-bounded attribute scanner that handles
both the HTML5 and http-equiv forms in any attribute order, first tag
wins. Drop the dormant, broken hts_getCharsetFromContentType.

Covered by -#test=metacharset/-#test=isutf8 engine self-tests, an
11-variant local-server crawl matrix with an update pass
(41_local-utf8-link.test), and new fuzz corpus seeds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* tests: international crawl tracked the double-encoded behavior

The default.html sub-crawl asserted the pre-fix output: two mojibake
café*.html filenames from double-encoded fetches and 4 errors where
the fix leaves 2 (the caf%e9.html Latin-1 escapes the server really
lacks). Update to the corrected names and counts; the other three
sub-crawls are unchanged. Only runs where ut.httrack.com is reachable
(in-tree builds), which is why just the deb CI job caught it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 21:06:28 +02:00
Xavier Roche
771d11326e Uncompressed body labeled gzip loses the page (#515)
* htszlib: keep an identity body mislabeled as compressed

A server sending Content-Encoding: gzip with a plain uncompressed body
lost the page: both inflate attempts fail, hts_zunpack returns -1, and
the mirror reports "Error when decompressing" with an empty file left
behind. Fall back to copying the body verbatim when neither deflate
framing decodes and the first bytes carry no gzip/zlib header; corrupt
or truncated compressed data still fails. Covered in the acceptencoding
self-test. Follow-up to the #47 investigation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* htszlib: fall back to identity only on a deflate data error

Adversarial review found two over-broad paths to the verbatim copy: a
truncated raw deflate stream (no header, exhausts input without a data
error) and any local failure (fwrite, inflateInit2, fseek, FOPEN) also
reached it, saving compressed garbage as the document with a success
return. Gate the fallback on the raw-deflate attempt hitting a data
error with no environmental failure. Selftest grows a multi-chunk
identity body and truncated gzip/zlib/raw-deflate negatives; the /big/
crawl now serves a mislabeled plain page end-to-end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 20:08:12 +02:00
Xavier Roche
7faceaf7d2 Remove the dead HTS_FAST_CACHE==0 code paths (#514)
HTS_FAST_CACHE has been hard-set to 1 since the coucal hashtable became
the cache index; the ==0 arms no longer even compile (they reference an
undeclared variable in cache_readex). Drop the knob, the dead arms, and
the cache_back 'use' field they read, which was never assigned.

No behavior change: the kept code is exactly what the preprocessor
selected, and the removed cache.use cleanup was a never-taken branch.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 19:03:59 +02:00
22 changed files with 605 additions and 186 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

@@ -0,0 +1 @@
<meta content="text/html; charset=gb2312" http-equiv="content-type">

View File

@@ -0,0 +1 @@
<meta charset='utf-8

View File

@@ -0,0 +1 @@
<meta charset = utf-8 />

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));
@@ -2050,13 +2059,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 +2204,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 +2393,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 +2415,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;
}
@@ -4145,38 +4128,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

@@ -84,7 +84,7 @@ static int hts_equalsAlphanum(const char *a, const char *b) {
/* Copy the memory region [s .. s + size - 1 ] as a \0-terminated string. */
static char *hts_stringMemCopy(const char *s, size_t size) {
char *dest = malloc(size + 1);
char *dest = malloct(size + 1);
if (dest != NULL) {
memcpy(dest, s, size);
@@ -549,42 +549,6 @@ char *hts_convertStringFromUTF8(const char *s, size_t size, const char *charset)
#endif
HTS_STATIC char *hts_getCharsetFromContentType(const char *mime) {
/* text/html; charset=utf-8 */
const char *const charset = "charset";
char *pos = strstr(mime, charset);
if (pos != NULL) {
/* Skip spaces */
int eq = 0;
for(pos += strlen(charset);
*pos == ' ' || *pos == '=' || *pos == '"' || *pos == '\''; pos++) {
if (*pos == '=') {
eq = 1;
}
}
if (eq == 1) {
int len;
for(len = 0;
pos[len] == ' ' || pos[len] == ';' || pos[len] == '"' || *pos == '\'';
pos++) ;
if (len != 0) {
char *const s = malloc(len + 1);
int i;
for(i = 0; i < len; i++) {
s[i] = pos[i];
}
s[len] = '\0';
return s;
}
}
}
return NULL;
}
#ifdef _WIN32
#define strcasecmp(a,b) stricmp(a,b)
#define strncasecmp(a,b,n) strnicmp(a,b,n)
@@ -644,58 +608,106 @@ int hts_isCharsetUTF8(const char *charset) {
|| strcasecmp(charset, "utf8") == 0 );
}
/* Extract X from a "text/html; charset=X" content= attribute value. */
static char *charset_from_content(const char *s, size_t len) {
size_t i;
for (i = 0; i + 7 < len; i++) {
if ((i == 0 || is_space(s[i - 1]) || s[i - 1] == ';') &&
strncasecmp(&s[i], "charset", 7) == 0 && is_space_or_equal(s[i + 7])) {
size_t j, val;
for (j = i + 7; j < len && is_space_or_equal_or_quote(s[j]); j++)
;
for (val = j; j < len && s[j] != '"' && s[j] != '\'' && s[j] != ';' &&
!is_space(s[j]);
j++)
;
if (j != val) {
return hts_stringMemCopy(&s[val], j - val);
}
}
}
return NULL;
}
char *hts_getCharsetFromMeta(const char *html, size_t size) {
int i;
size_t i;
/* <META HTTP-EQUIV="CONTENT-TYPE" CONTENT="text/html; charset=utf-8" > */
for(i = 0; i < size; i++) {
if (html[i] == '<' && strncasecmp(&html[i + 1], "meta", 4) == 0
&& is_space(html[i + 5])) {
/* Skip spaces */
for(i += 5; is_space(html[i]); i++) ;
if (strncasecmp(&html[i], "HTTP-EQUIV", 10) == 0
&& is_space_or_equal(html[i + 10])) {
for(i += 10; is_space_or_equal_or_quote(html[i]); i++) ;
if (strncasecmp(&html[i], "CONTENT-TYPE", 12) == 0) {
for(i += 12; is_space_or_equal_or_quote(html[i]); i++) ;
if (strncasecmp(&html[i], "CONTENT", 7) == 0
&& is_space_or_equal(html[i + 7])) {
for(i += 7; is_space_or_equal_or_quote(html[i]); i++) ;
/* Skip content-type */
for(;
i < size && html[i] != ';' && html[i] != '"' && html[i] != '\'';
i++) ;
/* Expect charset attribute here */
if (html[i] == ';') {
for(i++; is_space(html[i]); i++) ;
/* Look for charset */
if (strncasecmp(&html[i], "charset", 7) == 0
&& is_space_or_equal(html[i + 7])) {
int len;
/* HTML5 <meta charset=X> or legacy
<meta http-equiv="Content-Type" content="text/html; charset=X">,
attributes in any order; first resolvable tag wins. */
for (i = 0; i + 6 < size; i++) {
size_t j;
const char *equiv = NULL, *content = NULL;
size_t equiv_len = 0, content_len = 0;
for(i += 7; is_space_or_equal(html[i]) || html[i] == '\'';
i++) ;
/* Charset */
for(len = 0;
i + len < size && html[i + len] != '"'
&& html[i + len] != '\'' && html[i + len] != ' '; len++) ;
/* No error ? */
if (len != 0 && i < size) {
char *const s = malloc(len + 1);
int j;
if (html[i] != '<' || strncasecmp(&html[i + 1], "meta", 4) != 0 ||
!is_space(html[i + 5])) {
continue;
}
/* Attribute scan, strictly bounded by size. */
for (j = i + 6; j < size && html[j] != '>';) {
size_t name, name_len, val = 0, val_len = 0;
for(j = 0; j < len; j++) {
s[j] = html[i + j];
}
s[len] = '\0';
return s;
}
}
}
if (is_space(html[j]) || html[j] == '/') {
j++;
continue;
}
for (name = j; j < size && html[j] != '=' && html[j] != '>' &&
html[j] != '/' && !is_space(html[j]);
j++)
;
name_len = j - name;
for (; j < size && is_space(html[j]); j++)
;
if (j < size && html[j] == '=') {
for (j++; j < size && is_space(html[j]); j++)
;
if (j < size && (html[j] == '"' || html[j] == '\'')) {
const char quote = html[j++];
for (val = j; j < size && html[j] != quote; j++)
;
if (j >= size) /* unterminated quote: drop the tag */
break;
val_len = j++ - val;
} else {
/* unquoted: ends at whitespace or '>' only (HTML5 prescan); '/'
belongs to the value except as the tail of a self-close */
for (val = j; j < size && !is_space(html[j]) && html[j] != '>'; j++)
;
val_len = j - val;
if (val_len != 0 && j < size && html[j] == '>' &&
html[val + val_len - 1] == '/') {
val_len--;
}
}
}
/* first occurrence of each attribute wins, as in browsers */
if (val_len != 0) {
if (name_len == 7 && strncasecmp(&html[name], "charset", 7) == 0) {
return hts_stringMemCopy(&html[val], val_len);
} else if (equiv == NULL && name_len == 10 &&
strncasecmp(&html[name], "http-equiv", 10) == 0) {
equiv = &html[val];
equiv_len = val_len;
} else if (content == NULL && name_len == 7 &&
strncasecmp(&html[name], "content", 7) == 0) {
content = &html[val];
content_len = val_len;
}
}
}
if (equiv_len == 12 && strncasecmp(equiv, "content-type", 12) == 0 &&
content != NULL) {
char *const s = charset_from_content(content, content_len);
if (s != NULL) {
return s;
}
}
i = j;
}
return NULL;
}
@@ -1140,17 +1152,45 @@ int hts_isStringUTF8(const char *s, size_t size) {
const unsigned char *const data = (const unsigned char*) s;
size_t i;
/* Strict RFC 3629: reject overlongs, surrogates, > U+10FFFF, 5/6-byte. */
for(i = 0 ; i < size ; ) {
/* Reader: can read bytes up to j */
#define RD ( i < size ? data[i++] : -1 )
const unsigned char c = data[i];
size_t len, k;
hts_UCS4 uc, min;
/* Writer: a malformed sequence means the string is not UTF-8 (return 0) */
#define WR(C) if ((C) == -1) { return 0; }
if (c < 0x80) {
i++;
continue;
} else if (c >= 0xc2 && c <= 0xdf) {
len = 2;
min = 0x80;
uc = c & 0x1f;
} else if (c >= 0xe0 && c <= 0xef) {
len = 3;
min = 0x800;
uc = c & 0x0f;
} else if (c >= 0xf0 && c <= 0xf4) {
len = 4;
min = 0x10000;
uc = c & 0x07;
} else { /* continuation byte, overlong C0/C1, or F5..FF lead */
return 0;
}
if (size - i < len) {
return 0;
}
for (k = 1; k < len; k++) {
const unsigned char cc = data[i + k];
/* Read Unicode character. */
READ_UNICODE(RD, WR);
#undef RD
#undef WR
if ((cc & 0xc0) != 0x80) {
return 0;
}
uc = (uc << 6) | (cc & 0x3f);
}
if (uc < min || uc > 0x10FFFF || (uc >= 0xD800 && uc <= 0xDFFF)) {
return 0;
}
i += len;
}
return 1;

View File

@@ -76,7 +76,8 @@ extern char *hts_convertStringIDNAToUTF8(const char *s, size_t size);
extern int hts_isStringIDNA(const char *s, size_t size);
/**
* Extract the charset from the HTML buffer "html"
* Extract the <meta> charset from the HTML buffer "html" (HTML5 charset= or
* legacy http-equiv form). Returns a malloc'ed string, or NULL if none.
**/
extern char *hts_getCharsetFromMeta(const char *html, size_t size);
@@ -86,7 +87,8 @@ extern char *hts_getCharsetFromMeta(const char *html, size_t size);
extern int hts_isStringAscii(const char *s, size_t size);
/**
* Is the given string an UTF-8 string ?
* Is the given string valid UTF-8 ? Strict RFC 3629: overlong forms,
* surrogates, codepoints above U+10FFFF and 5/6-byte sequences are rejected.
**/
extern int hts_isStringUTF8(const char *s, size_t size);

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"

View File

@@ -2007,7 +2007,7 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
/* Unescape/escape %20 and other &nbsp; */
{
// Note: always true (iso-8859-1 as default)
// NULL when UTF-8 conversion is off (-%T0)
const char *const charset = str->page_charset_;
const int hasCharset = charset != NULL
&& *charset != '\0';
@@ -2030,11 +2030,12 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
// Force to encode non-printable chars (should never happend)
escape_remove_control(lien);
// charset conversion for the URI filename,
// and not already UTF-8
// (note: not for the query string!)
if (hasCharset && !hts_isCharsetUTF8(charset)) {
// charset conversion for the URI filename (not the query
// string), unless the bytes already are valid UTF-8:
// converting those would double-encode them (#180)
if (hasCharset && !hts_isCharsetUTF8(charset) &&
!hts_isStringUTF8(lien, strlen(lien))) {
char *const s = hts_convertStringToUTF8(lien, strlen(lien), charset);
if (s != NULL) {
hts_log_print(opt, LOG_DEBUG,

View File

@@ -788,6 +788,30 @@ static int st_charset(httrackp *opt, int argc, char **argv) {
return 0;
}
static int st_metacharset(httrackp *opt, int argc, char **argv) {
char *s;
(void) opt;
if (argc < 1) {
fprintf(stderr, "metacharset: needs an html string\n");
return 1;
}
s = hts_getCharsetFromMeta(argv[0], strlen(argv[0]));
printf("%s\n", s != NULL ? s : "(none)");
freet(s);
return 0;
}
static int st_isutf8(httrackp *opt, int argc, char **argv) {
(void) opt;
if (argc < 1) {
fprintf(stderr, "isutf8: needs a string\n");
return 1;
}
printf("%d\n", hts_isStringUTF8(argv[0], strlen(argv[0])) ? 1 : 0);
return 0;
}
static int st_idna_encode(httrackp *opt, int argc, char **argv) {
char *s;
@@ -2123,6 +2147,19 @@ static int ae_write_collision(const char *path, const unsigned char *src,
return ok ? 0 : 1;
}
/* Write src[0..len) to path as-is; 0 on success. */
static int ae_write_raw(const char *path, const unsigned char *src,
size_t len) {
FILE *const f = FOPEN(path, "wb");
int ok;
if (f == NULL)
return 1;
ok = fwrite(src, 1, len, f) == len;
fclose(f);
return ok ? 0 : 1;
}
/* Compare path's bytes to expect[0..len); 0 if equal. Streams (large files). */
static int ae_check_decoded(const char *path, const unsigned char *expect,
size_t len) {
@@ -2190,6 +2227,46 @@ static int st_acceptencoding(httrackp *opt, int argc, char **argv) {
assertf(ae_write_collision(inpath, body, 64) == 0);
assertf(hts_zunpack(inpath, outpath) == 64);
assertf(ae_check_decoded(outpath, body, 64) == 0);
/* Identity fallback (#47): a plain body mislabeled as compressed is kept
verbatim, small and multi-chunk (> one 8 KiB fread). */
assertf(ae_write_raw(inpath, small, slen) == 0);
assertf(hts_zunpack(inpath, outpath) == (int) slen);
assertf(ae_check_decoded(outpath, small, slen) == 0);
{
const size_t ilen = 16 * 1024;
unsigned char *idbody = malloct(ilen);
assertf(idbody != NULL);
for (i = 0; i < ilen; i++)
idbody[i] = small[i % slen];
assertf(ae_write_raw(inpath, idbody, ilen) == 0);
assertf(hts_zunpack(inpath, outpath) == (int) ilen);
assertf(ae_check_decoded(outpath, idbody, ilen) == 0);
freet(idbody);
}
/* Truncated gzip (CRC+ISIZE cut), zlib (ADLER32 cut) and raw deflate
must all still fail, not fall back to a verbatim copy. */
{
static const struct {
int wb;
size_t cut;
} tr[] = {{16 + MAX_WBITS, 8}, {MAX_WBITS, 4}, {-MAX_WBITS, 5}};
for (i = 0; i < sizeof(tr) / sizeof(tr[0]); i++) {
unsigned char z[512];
size_t zlen;
FILE *f;
assertf(ae_write_packed(inpath, tr[i].wb, small, slen) == 0);
f = FOPEN(inpath, "rb");
assertf(f != NULL);
zlen = fread(z, 1, sizeof(z), f);
fclose(f);
assertf(zlen > tr[i].cut && zlen < sizeof(z));
assertf(ae_write_raw(inpath, z, zlen - tr[i].cut) == 0);
assertf(hts_zunpack(inpath, outpath) < 0);
}
}
freet(body);
}
#else
@@ -2394,6 +2471,9 @@ static const struct selftest_entry {
{"mime", "<filename>", "MIME type for a filename", st_mime},
{"charset", "<charset> <string>",
"convert a string to UTF-8 from a charset", st_charset},
{"metacharset", "<html>", "extract the <meta> charset from an HTML page",
st_metacharset},
{"isutf8", "<string>", "is the string valid UTF-8 (1/0)", st_isutf8},
{"idna-encode", "<host>", "encode a hostname to IDNA/punycode",
st_idna_encode},
{"idna-decode", "<host>", "decode an IDNA/punycode hostname",

View File

@@ -48,6 +48,7 @@ Please visit our Website: http://www.httrack.com
/*
Unpack file into a new file (gzip, zlib RFC1950 or raw deflate RFC1951).
A body provably in no deflate framing is copied verbatim (identity).
Return value: size of the new file, or -1 if an error occurred
*/
/* Note: utf-8 */
@@ -68,6 +69,10 @@ int hts_zunpack(char *filename, char *newfile) {
((inbuf[0] & 0x0f) == Z_DEFLATED &&
(((unsigned) inbuf[0] << 8 | inbuf[1]) % 31) == 0)));
int attempt;
/* not_deflate: the raw attempt hit a data error, so the body is in no
deflate framing at all. env_error: local I/O or memory failure. */
hts_boolean not_deflate = HTS_FALSE;
hts_boolean env_error = HTS_FALSE;
/* deflate is ambiguous; on failure retry with the other windowBits */
for (attempt = 0; attempt < 2 && ret < 0; attempt++) {
@@ -78,15 +83,20 @@ int hts_zunpack(char *filename, char *newfile) {
if (attempt > 0) {
/* rewind input; reopening fpout "wb" discards the partial output */
if (fseek(in, 0, SEEK_SET) != 0)
if (fseek(in, 0, SEEK_SET) != 0) {
env_error = HTS_TRUE;
break;
}
navail = fread(inbuf, 1, sizeof(inbuf), in);
}
fpout = FOPEN(fconv(catbuff, sizeof(catbuff), newfile), "wb");
if (fpout == NULL)
if (fpout == NULL) {
env_error = HTS_TRUE;
break;
}
memset(&strm, 0, sizeof(strm));
if (inflateInit2(&strm, windowBits) != Z_OK) {
env_error = HTS_TRUE;
fclose(fpout);
break;
}
@@ -108,12 +118,17 @@ int hts_zunpack(char *filename, char *newfile) {
zerr = inflate(&strm, Z_NO_FLUSH);
if (zerr == Z_NEED_DICT || zerr == Z_DATA_ERROR ||
zerr == Z_MEM_ERROR || zerr == Z_STREAM_ERROR) {
if (zerr == Z_MEM_ERROR || zerr == Z_STREAM_ERROR)
env_error = HTS_TRUE;
else if (windowBits < 0)
not_deflate = HTS_TRUE;
ok = HTS_FALSE;
break;
}
produced = sizeof(outbuf) - strm.avail_out;
if (produced > 0 &&
fwrite(outbuf, 1, produced, fpout) != produced) {
env_error = HTS_TRUE;
ok = HTS_FALSE;
break;
}
@@ -129,6 +144,28 @@ int hts_zunpack(char *filename, char *newfile) {
inflateEnd(&strm);
fclose(fpout);
}
/* keep a mislabeled identity body verbatim only when provably not
deflate; truncation or a local failure must keep failing (#47) */
if (ret < 0 && !wrapped && not_deflate && !env_error && !ferror(in)) {
FILE *const fpout =
FOPEN(fconv(catbuff, sizeof(catbuff), newfile), "wb");
if (fpout != NULL && fseek(in, 0, SEEK_SET) == 0) {
int size = 0;
while ((navail = fread(inbuf, 1, sizeof(inbuf), in)) > 0) {
if (fwrite(inbuf, 1, navail, fpout) != navail) {
size = -1;
break;
}
size += (int) navail;
}
if (size >= 0 && !ferror(in))
ret = size;
}
if (fpout != NULL)
fclose(fpout);
}
fclose(in);
}
}

View File

@@ -36,3 +36,64 @@ test "$(httrack -O /dev/null -#test=charset 'no-such-charset-xyz' 'café' 2>/dev
# malformed UTF-8 (lone continuation byte, truncated lead byte) must not crash
runs 'utf-8' $'\x80'
runs 'utf-8' $'\xc3'
# hts_getCharsetFromMeta: <meta> charset extraction, HTML5 and legacy forms.
# -#test=metacharset <html> prints the charset, or "(none)".
meta() {
test "$(httrack -O /dev/null -#test=metacharset "$1")" == "$2" || exit 1
}
# HTML5 form, quoting/spacing flavors
meta '<meta charset="utf-8">' 'utf-8'
meta "<meta charset='utf-8'>" 'utf-8'
meta '<meta charset=utf-8>' 'utf-8'
meta '<meta charset=utf-8 />' 'utf-8'
meta '<meta charset=utf-8/>' 'utf-8'
# charset labels are case-insensitive downstream; only require case-insensitive match
test "$(httrack -O /dev/null -#test=metacharset '<META CHARSET=UTF-8>' | tr '[:upper:]' '[:lower:]')" == 'utf-8' || exit 1
meta '<html><head><meta charset = "utf-8" ></head>' 'utf-8'
# legacy http-equiv form, attributes in any order
meta '<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">' 'iso-8859-1'
meta '<meta content="text/html; charset=gb2312" http-equiv="content-type">' 'gb2312'
meta '<meta http-equiv=Content-Type content="text/html;charset=euc-jp">' 'euc-jp'
meta '<meta http-equiv="Content-Type" content="text/html; charset = koi8-r ">' 'koi8-r'
meta '<meta http-equiv="Content-Type" content="text/html; charset=utf-8;">' 'utf-8'
meta '<meta http-equiv=content-type content=text/html;charset=utf-8 />' 'utf-8'
meta '<meta http-equiv=content-type content=text/html;charset=utf-8>' 'utf-8'
# first resolvable meta wins; unrelated metas are skipped
meta '<meta name="viewport" content="width=1"><meta charset="koi8-r">' 'koi8-r'
meta '<meta charset="utf-8"><meta http-equiv="Content-Type" content="text/html; charset=gb2312">' 'utf-8'
# duplicate attribute: first wins, as in browsers
meta '<meta http-equiv="Content-Type" content="text/html; charset=big5" http-equiv="refresh">' 'big5'
# negatives: wrong tag/attribute, empty value
meta '<p>x</p>' '(none)'
meta '<meta name="desc" content="charset=bogus">' '(none)'
meta '<meta http-equiv="refresh" content="0; url=x.html">' '(none)'
meta '<body charset="utf-8">' '(none)'
meta '<meta charset="">' '(none)'
# hostile truncations: no crash, no match
meta '<meta charset="utf-8' '(none)'
meta '<meta charset=' '(none)'
meta '<meta ' '(none)'
meta '<meta' '(none)'
# hts_isStringUTF8: valid-UTF-8 probe (guards the #180 link conversion).
utf8() {
test "$(httrack -O /dev/null -#test=isutf8 "$1")" == "$2" || exit 1
}
utf8 'plain' '1'
utf8 'café' '1'
utf8 '统计' '1'
utf8 "$(printf 'caf\xe9')" '0' # latin-1 accent
utf8 "$(printf '\xc3')" '0' # truncated lead byte
utf8 "$(printf '\xa9')" '0' # lone continuation byte
utf8 "$(printf '\xf0\x9f\x98\x80')" '1' # 4-byte emoji
utf8 "$(printf '\xc0\xaf')" '0' # overlong (latin-1 "À¯" must stay convertible)
utf8 "$(printf '\xed\xa0\x80')" '0' # UTF-16 surrogate
utf8 "$(printf '\xf4\x90\x80\x80')" '0' # above U+10FFFF
utf8 "$(printf '\xf8\x88\x80\x80\x80')" '0' # legacy 5-byte form

View File

@@ -22,9 +22,9 @@ bash crawl-test.sh \
httrack http://ut.httrack.com/unicode-links/utf8.html
bash crawl-test.sh \
--errors 4 --files 7 \
--found ut.httrack.com/unicode-links/café3860.html \
--found ut.httrack.com/unicode-links/café9fa8.html \
--errors 2 --files 9 \
--found ut.httrack.com/unicode-links/café3860.html \
--found ut.httrack.com/unicode-links/café9fa8.html \
--found ut.httrack.com/unicode-links/café30f4.html \
--found ut.httrack.com/unicode-links/café5e1f.html \
--found ut.httrack.com/unicode-links/café7b30.html \

View File

@@ -1,7 +1,7 @@
#!/bin/bash
#
# Diverse seeded /big/ crawl: 12 pattern families, decoy absence, update pass
# must 304-revalidate. 360 = 1 index + 96 pages + 192 imgs + 5 shared + 60
# must 304-revalidate. 361 = 1 index + 96 pages + 192 imgs + 5 shared + 61
# family + 6 singles; the 4 planted errors write -o1 pages, not counted.
set -euo pipefail
@@ -9,7 +9,7 @@ set -euo pipefail
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" --rerun \
--errors 4 --files 360 \
--errors 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' \
@@ -40,6 +40,7 @@ bash "$top_srcdir/tests/local-crawl.sh" --rerun \
--not-found 'big/x/bj.jar' \
--not-found 'big/x/is1.png' \
--not-found 'big/x/concat.html' \
--file-matches 'big/f1/gzid.html' '<p>gzid</p>' \
--file-matches 'big/p/2.html' 'srcset="\.\./a/f2-1x\.png 1x, \.\./a/f2-2x\.png 2x"' \
--file-matches 'big/a/blk2.css' 'url\(blk2-bg\.png\)' \
--file-matches 'big/p/5.html' "document\\.write\\('<a href=\"\\.\\./f5/dw\\.html\"" \

View File

@@ -0,0 +1,21 @@
#!/bin/bash
#
# #180: raw non-ASCII hrefs must reach the wire UTF-8 percent-encoded whatever
# the declared page charset; each PDF exists only at its exact UTF-8 path.
set -euo pipefail
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 --rerun \
--found 'charset/header/统计大数据服务平台.pdf' \
--found 'charset/meta5/统计大数据服务平台.pdf' \
--found 'charset/metaeq/统计大数据服务平台.pdf' \
--found 'charset/none/统计大数据服务平台.pdf' \
--found 'charset/latin1hdr/统计大数据服务平台.pdf' \
--found 'charset/latin1real/café.pdf' \
--found 'charset/metalatin1/déjà.pdf' \
--found 'charset/latin1ovl/À¡x.pdf' \
--found 'charset/priority/nuée.pdf' \
--found 'charset/preenc/统计大数据服务平台.pdf' \
--found 'charset/bom/统计大数据服务平台.pdf' \
httrack 'BASEURL/charset/index.html'

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

@@ -109,6 +109,8 @@ TESTS = \
37_local-cache-outage.test \
38_local-update-304.test \
39_local-delayed-cancel.test \
40_local-why.test
40_local-why.test \
41_local-utf8-link.test \
42_local-maxsize-slow.test
CLEANFILES = check-network_sh.cache

View File

@@ -374,6 +374,7 @@ def big_index(port):
'<img src="/big/a/d1/d2/d3/d4/d5/d6/d7/d8/deep.png">'
'<a href="/big/f1/long.html?x=%s">long</a>'
'<a href="/big/f1/gzok.html">gzok</a>'
'<a href="/big/f1/gzid.html">gzid</a>'
'<a href="//127.0.0.1:%d/big/f1/protorel.html">protorel</a>'
'<a href="http://127.0.0.1:%d/big/f1/abshost.html">abshost</a>'
'<a href="/big/e/404.html">e404</a>'
@@ -399,6 +400,7 @@ BIG_SIMPLE_PAGES = {
"/big/f1/dir/": "dir index",
"/big/f1/long.html": "long",
"/big/f1/gzok.html": "gzok",
"/big/f1/gzid.html": "gzid",
"/big/f1/protorel.html": "protorel",
"/big/f1/abshost.html": "abshost",
"/big/f5/dw.html": "dw target",
@@ -660,6 +662,97 @@ class Handler(SimpleHTTPRequestHandler):
def route_intl_page(self):
self.send_raw(b"<html><body>accented page</body></html>\n", "text/html")
# Raw non-ASCII href matrix (#180): each variant declares the page charset
# differently; the PDF exists only at its exact UTF-8 path, so a
# mis-decoded link 404s.
CHARSET_CJK = "统计大数据服务平台.pdf"
# variant -> (index Content-Type, <head> bytes, href bytes, pdf name)
CHARSET_VARIANTS = {
"header": ("text/html; charset=utf-8", b"", CHARSET_CJK.encode(), CHARSET_CJK),
"meta5": (
"text/html",
b'<meta charset="utf-8">',
CHARSET_CJK.encode(),
CHARSET_CJK,
),
"metaeq": (
"text/html",
b'<meta http-equiv="Content-Type" content="text/html; charset=utf-8">',
CHARSET_CJK.encode(),
CHARSET_CJK,
),
"none": ("text/html", b"", CHARSET_CJK.encode(), CHARSET_CJK),
"latin1hdr": (
"text/html; charset=iso-8859-1",
b"",
CHARSET_CJK.encode(),
CHARSET_CJK,
),
# genuine latin-1 href: the charset conversion must still apply
"latin1real": (
"text/html; charset=iso-8859-1",
b"",
"café.pdf".encode("latin-1"),
"café.pdf",
),
# latin-1 declared by META only: the meta parser is load-bearing
"metalatin1": (
"text/html",
b'<meta charset="iso-8859-1">',
"déjà.pdf".encode("latin-1"),
"déjà.pdf",
),
# latin-1 bytes that form an overlong UTF-8 shape: strict validation
# must still convert them
"latin1ovl": (
"text/html; charset=iso-8859-1",
b"",
"À¡x.pdf".encode("latin-1"),
"À¡x.pdf",
),
# header wins over meta: latin-1 href only resolves if iso-8859-1 is kept
"priority": (
"text/html; charset=iso-8859-1",
b'<meta charset="utf-8">',
"nuée.pdf".encode("latin-1"),
"nuée.pdf",
),
"preenc": ("text/html", b"", quote(CHARSET_CJK).encode(), CHARSET_CJK),
"bom": ("text/html", b"", CHARSET_CJK.encode(), CHARSET_CJK),
}
def route_charset(self):
path = unquote(urlsplit(self.path).path)
parts = path.split("/")
if path == "/charset/index.html":
self.send_html(
"".join(
'\t<a href="%s/index.html">%s</a>\n' % (v, v)
for v in self.CHARSET_VARIANTS
)
)
return
if len(parts) == 4 and parts[2] in self.CHARSET_VARIANTS:
ctype, head, href, pdf = self.CHARSET_VARIANTS[parts[2]]
if parts[3] == "index.html":
body = (
b"<html><head>"
+ head
+ b'</head><body><a href="'
+ href
+ b'">doc</a></body></html>'
)
if parts[2] == "bom":
body = b"\xef\xbb\xbf" + body
self.send_raw(body, ctype)
return
if parts[3] == pdf:
self.send_raw(self.FAKE_PDF, "application/pdf")
return
self.send_response(404)
self.send_header("Content-Length", "0")
self.end_headers()
# resume / 416 loop (#206): the first GET stalls after a prefix so the crawl
# can be interrupted (partial + temp-ref); every later request is 416.
RESUME_PREFIX = b"PARTIAL-" + b"x" * 4096 # flushed before the stall
@@ -942,6 +1035,14 @@ 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))
)
ROUTES = {
"/cookies/entrance.php": route_entrance,
"/cookies/second.php": route_second,
@@ -1014,6 +1115,11 @@ 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,
"/delayed/noloc.php": route_delayed_noloc,
"/delayed/selfloop.php": route_delayed_selfloop,
"/delayed/redir.php": route_delayed_redir,
@@ -1079,6 +1185,13 @@ class Handler(SimpleHTTPRequestHandler):
"text/html",
extra=[("Content-Encoding", "gzip")],
)
elif path == "/big/f1/gzid.html":
# Plain body mislabeled as gzip: identity fallback keeps it (#47)
self.big_send(
body,
"text/html",
extra=[("Content-Encoding", "gzip")],
)
else:
self.big_send(body, "text/html")
elif path == "/big/f1/list.html":
@@ -1194,6 +1307,9 @@ class Handler(SimpleHTTPRequestHandler):
if path.startswith("/big/"):
self.route_big()
return True
if path.startswith("/charset/"):
self.route_charset()
return True
# Match percent-encoded paths (accented #157 route) by their decoded form.
handler = self.ROUTES.get(path) or self.ROUTES.get(unquote(path))
if handler is not None: