2 Commits

Author SHA1 Message Date
Xavier Roche
ca7c30666a tests: make the #911 leak check see stdout and the property, not two literals
The absence checks pinned 'DEBUG: DAV-DATA' and '^RESPONSE:', which four
re-added variants walk straight past: a trace without the hyphen, one with no
marker at all, one prefixed so the '^' misses, and one on stdout. The stdout
case is the worst of them: redirected to a file, proxytrack's stdout is fully
buffered and SIGTERM never flushes it, so the leak never reached the log the
test reads.

Give proxytrack a pty instead of a file, so libc line-buffers its output on
Linux and macOS alike, and assert the property: a PROPFIND carries a canary the
index cannot produce, and neither the canary nor a distinctive string from the
generated response may appear in what proxytrack wrote. The two literals stay as
names for the specific regression. The liveness guard now requires a PROPFIND
answered 207, since a depth-rejected one is logged 403 by the shared reply path
without ever reaching the deleted code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-08-02 12:12:59 +02:00
Xavier Roche
1d8c429545 proxytrack: drop the leftover debug traces on the WebDAV path
Two fprintf(stderr) calls in the PROPFIND path shipped by accident: one dumped
the client-supplied request body, the other the whole generated multistatus
response. Both ran on every PROPFIND with no authentication in front of them,
so any client could write bytes of its choosing into the operator's stderr.
The body is never parsed and the response is derivable from the index, so
neither trace has diagnostic value worth keeping behind a debug level.

Closes #911

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-08-02 11:48:07 +02:00
16 changed files with 228 additions and 951 deletions

View File

@@ -265,20 +265,17 @@ jobs:
# cause a false mismatch either.
# footer-overflow and purge-longpath skip on Windows (need a path past MAX_PATH);
# crange pending #581;
# webdav-default, webdav-mime and webdav-overflow need a reapable background listener, which MSYS cannot give them;
# webdav-default and webdav-mime need a reapable background listener, which MSYS cannot give them;
# badmtime needs a filesystem that stores an mtime past gmtime's range;
# single-file ends on a GUI half needing htsserver, which this job does not build;
# update-304-leak needs a LeakSanitizer build, which MSVC has no equivalent of;
# crash-symbolize and backtrace-empty need backtrace(), which Windows has no
# equivalent of;
# string-oom drives a helper binary that only the automake build produces.
# equivalent of.
expected_skips="01_engine-footer-overflow.test
100_local-purge-longpath.test
114_local-update-304-leak.test
120_local-proxytrack-webdav-default.test
143_engine-backtrace-empty.test
147_local-proxytrack-webdav-overflow.test
152_engine-string-oom.test
48_local-crange-memresume.test
71_local-crange-repaircache.test
79_local-proxytrack-webdav-mime.test

View File

@@ -829,14 +829,6 @@ static hts_boolean back_chunked_unterminated(const lien_back *const back) {
return back->is_chunk && back->chunk_blocksize != -1 ? HTS_TRUE : HTS_FALSE;
}
/* Past the terminating chunk, the line still owed is the optional trailer
section (RFC 9112 7.1.2), read and discarded like a header block. */
static hts_boolean back_in_chunk_trailers(const lien_back *const back) {
return back->status == STATUS_CHUNK_CR && back->chunk_blocksize == -1
? HTS_TRUE
: HTS_FALSE;
}
// objet (lien) téléchargé ou transféré depuis le cache
//
// fermer les paramètres de transfert,
@@ -3498,10 +3490,6 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
else if (back[i].status == STATUS_CHUNK_WAIT || back[i].status == STATUS_CHUNK_CR) { // recevoir longueur chunk en hexa caractère par caractère
// backuper pour lire dans le buffer chunk
htsblk r;
/* Block mode bounds the trailer section, which declares no length
of its own, by HTS_LINE_BLOCK_SIZE. */
const int chunk_read_mode =
back_in_chunk_trailers(&back[i]) ? 0 : -1;
memcpy(&r, &(back[i].r), sizeof(htsblk));
back[i].r.is_write = 0; // mémoire
@@ -3512,7 +3500,7 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
back[i].r.is_file = 0;
//
// ligne par ligne
retour_fread = http_xfread1(&(back[i].r), chunk_read_mode);
retour_fread = http_xfread1(&(back[i].r), -1);
// modifier et restaurer
back[i].chunk_adr = back[i].r.adr; // adresse
back[i].chunk_size = back[i].r.size; // taille taille chunk
@@ -3650,22 +3638,12 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
}
// Traitement des en têtes chunks ou en têtes
if (back[i].status == STATUS_CHUNK_WAIT || back[i].status == STATUS_CHUNK_CR) { // réception taille chunk en hexa ( après les en têtes, peut ne pas
const hts_boolean in_trailers = back_in_chunk_trailers(&back[i]);
/* A chunk-size or chunk-CRLF line closes on its first LF, the
trailer section on the blank line ending it. Two LFs mean a
blank line only because the reader drops every CR. */
if (back[i].chunk_size > 0 &&
back[i].chunk_adr[back[i].chunk_size - 1] == 10 &&
(!in_trailers || back[i].chunk_size == 1 ||
back[i].chunk_adr[back[i].chunk_size - 2] == 10)) {
if (back[i].chunk_size > 0
&& back[i].chunk_adr[back[i].chunk_size - 1] == 10) {
int chunk_size = -1;
char chunk_data[64];
if (in_trailers) {
chunk_size =
0; /* fields discarded, the blank line ends the body */
} else if (back[i].chunk_size < 32) { // not too big
if (back[i].chunk_size < 32) { // pas trop gros
char *chstrip = back[i].chunk_adr;
back[i].chunk_adr[back[i].chunk_size - 1] = '\0'; // octet nul
@@ -3849,6 +3827,12 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
}
}
}
/* Oops, trailers! */
if (back[i].r.keep_alive_trailers) {
/* fixme (not yet supported) */
}
}
}
@@ -3862,7 +3846,7 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
// NO! xxback[i].chunk_blocksize = 0;
}
} // chunk buffer holds a complete line
} // taille buffer chunk > 1 && LF
//
} else if (back[i].status == STATUS_WAIT_HEADERS) { // en têtes (avant le chunk si il est présent)
//

View File

@@ -1954,7 +1954,7 @@ LLint http_xfread1(htsblk * r, int bufl) {
} else if (bufl == -2) { // force reserve
if (r->adr == NULL) {
r->adr = (char *) malloct(HTS_LINE_BLOCK_SIZE);
r->adr = (char *) malloct(8192);
r->size = 0;
return 0;
}
@@ -1969,11 +1969,11 @@ LLint http_xfread1(htsblk * r, int bufl) {
nl = READ_INTERNAL_ERROR;
count--;
if (r->adr == NULL) {
r->adr = (char *) malloct(HTS_LINE_BLOCK_SIZE);
r->adr = (char *) malloct(8192);
r->size = 0;
}
if (r->adr != NULL) {
if (r->size < HTS_LINE_BLOCK_SIZE - 2) {
if (r->size < 8190) {
// lecture
nl = hts_read(r, r->adr + r->size, 1);
if (nl > 0) {

View File

@@ -248,10 +248,6 @@ void treathead(t_cookie * cookie, const char *adr, const char *fil, htsblk * ret
void treatfirstline(htsblk * retour, const char *rcvd);
// sous-fonctions
/* Buffer http_xfread1() fills in its line modes, and so the ceiling on any
blank-line-terminated block it reads: a header section or a chunk trailer
section. Overrunning it fails the transfer. */
#define HTS_LINE_BLOCK_SIZE 8192
LLint http_xfread1(htsblk * r, int bufl);
/* Cached resolver: fill out[0..count-1] with up to max addresses for iadr (in
resolver order), returning the count (0 = does not resolve, negative-cached).

View File

@@ -80,7 +80,6 @@ Please visit our Website: http://www.httrack.com
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#ifndef _WIN32
#include <sys/socket.h>
#include <unistd.h>
@@ -7632,149 +7631,6 @@ static int st_rtrim(httrackp *opt, int argc, char **argv) {
return err;
}
/* Format LEN bytes of EXPECTED into S as two arguments, and check what came
back. HEAD and TAIL are scratch buffers of at least LEN+1 bytes. */
static int strsprintf_case(String *s, const char *expected, size_t len,
char *head, char *tail) {
const size_t half = len / 2;
memcpy(head, expected, half);
head[half] = '\0';
memcpy(tail, expected + half, len - half);
tail[len - half] = '\0';
StringSprintf(*s, "%s%s", head, tail);
return StringLength(*s) == len &&
memcmp(StringBuff(*s), expected, len) == 0 &&
StringBuff(*s)[len] == '\0';
}
/* StringSprintf_ stores the terminator at buffer[ret], so its `ret < capacity`
guard is off by one byte at the exact fill: an output whose length equals the
capacity writes past the allocation (#836). The lengths that reach it are the
capacities themselves, floored at 256 and doubling from there. */
static int st_strsprintf(httrackp *opt, int argc, char **argv) {
static const size_t caps[] = {256, 512, 1024, 2048};
enum { maxLen = 2100 };
char *expected = malloct(maxLen + 1);
char *head = malloct(maxLen + 1);
char *tail = malloct(maxLen + 1);
String reused = STRING_EMPTY;
size_t i, len;
int err = 0;
(void) opt;
(void) argc;
(void) argv;
if (expected == NULL || head == NULL || tail == NULL) {
printf("strsprintf self-test: FAIL (out of memory)\n");
return 1;
}
for (i = 0; i < maxLen; i++)
expected[i] = (char) ('a' + (i % 26));
expected[maxLen] = '\0';
/* one call on a String whose capacity is pinned to the boundary, so len ==
capacity is reached exactly once per boundary */
for (i = 0; !err && i < sizeof(caps) / sizeof(caps[0]); i++) {
for (len = caps[i] - 3; !err && len <= caps[i] + 3; len++) {
String s = STRING_EMPTY;
StringRoomTotal(s, caps[i]);
if (StringCapacity(s) != caps[i]) {
printf(" FAIL: capacity %u pinned to %u\n", (unsigned) caps[i],
(unsigned) StringCapacity(s));
err = 1;
} else if (!strsprintf_case(&s, expected, len, head, tail)) {
printf(" FAIL: length %u at capacity %u\n", (unsigned) len,
(unsigned) caps[i]);
err = 1;
}
StringFree(s);
}
}
/* the same String reused: its capacity grows under it between calls, and a
shorter output must not leave the previous one behind */
for (len = 0; !err && len <= maxLen; len++) {
if (!strsprintf_case(&reused, expected, len, head, tail)) {
printf(" FAIL: growing length %u\n", (unsigned) len);
err = 1;
}
}
for (len = maxLen + 1; !err && len-- > 0;) {
if (!strsprintf_case(&reused, expected, len, head, tail)) {
printf(" FAIL: shrinking length %u\n", (unsigned) len);
err = 1;
}
}
StringFree(reused);
/* The give-up path: an argument libc cannot convert fails at every capacity,
so the retry loop climbs to STRING_SPRINTF_MAX and then empties the
String. Probe libc first -- a platform that formats an unpaired surrogate
without faulting never reaches the path. */
{
static const wchar_t bad[] = {(wchar_t) 0xd800, 0};
char probe[32];
if (snprintf(probe, sizeof(probe), "%ls", bad) < 0) {
String s = STRING_EMPTY;
StringCopy(s, "leftover");
StringSprintf(s, "%ls", bad);
if (StringNotEmpty(s) || StringBuff(s) == NULL ||
StringBuff(s)[0] != '\0') {
printf(" FAIL: a failed conversion left %u bytes behind\n",
(unsigned) StringLength(s));
err = 1;
}
StringFree(s);
} else { /* stderr: test 150 pins stdout to the one-line verdict */
fprintf(stderr, " (skipped: this libc formats an unconvertible wide "
"string)\n");
}
}
/* StringSprintf empties the String when it gives up, and the WebDAV
enumeration pops the trailing '/' right after: on an empty String an
unguarded pop would wrap the unsigned length and write off the end. */
{
String never = STRING_EMPTY;
String cleared = STRING_EMPTY;
StringPopRight(never); /* never written to: buffer_ is still NULL */
if (StringLength(never) != 0 || StringBuff(never) != NULL) {
printf(" FAIL: pop on an unallocated String\n");
err = 1;
}
StringClear(cleared);
StringPopRight(cleared);
if (StringLength(cleared) != 0 || StringBuff(cleared)[0] != '\0') {
printf(" FAIL: pop on an emptied String\n");
err = 1;
}
/* control: the guard must not swallow a pop that has a byte to drop */
StringSprintf(cleared, "ab");
StringPopRight(cleared);
if (StringLength(cleared) != 1 || strcmp(StringBuff(cleared), "a") != 0) {
printf(" FAIL: pop on a non-empty String\n");
err = 1;
}
StringFree(never);
StringFree(cleared);
}
freet(expected);
freet(head);
freet(tail);
printf("strsprintf self-test: %s\n", err ? "FAIL" : "OK");
return err;
}
/* ------------------------------------------------------------ */
/* Registry: name -> handler, with a usage hint and a one-line description. */
/* ------------------------------------------------------------ */
@@ -7829,8 +7685,6 @@ static const struct selftest_entry {
{"hashtable", "<count|file>", "coucal hashtable stress test", st_hashtable},
{"strsafe", "[overflow|overflow-buff|overflow-src [str]]",
"bounded string-op self-test", st_strsafe},
{"strsprintf", "", "StringSprintf grows to fit at every capacity boundary",
st_strsprintf},
{"copyopt", "", "copy_htsopt option-copy self-test", st_copyopt},
{"lastchar", "",
"last-char helpers never index before the buffer (#770, #781, #821)",

View File

@@ -36,9 +36,6 @@ Please visit our Website: http://www.httrack.com
#define HTS_STRINGS_DEFSTATIC
/* System definitions. */
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* GCC extension */
@@ -94,6 +91,10 @@ struct String {
#define STRING_FREE(BUFF) free(BUFF)
#endif
#ifndef STRING_ASSERT
#include <assert.h>
#define STRING_ASSERT(EXP) assert(EXP)
#endif
/** Initializer for an empty String (NULL buffer). Use to declare or reset. **/
#define STRING_EMPTY {(char *) NULL, 0, 0}
@@ -126,44 +127,27 @@ struct String {
/** Byte POS positions from the end (read/write). POS==1 is the last byte. **/
#define StringRightRW(BLK, POS) (StringBuffRW(BLK)[StringLength(BLK) - POS])
/** Drop the last byte and re-terminate. No-op on an empty String: the length
is unsigned, so an unguarded pop would wrap it and write off the end. **/
/** Drop the last byte and re-terminate. Undefined if the String is empty
(no length check; would underflow). **/
#define StringPopRight(BLK) \
do { \
if (StringLength(BLK) > 0) { \
StringBuffRW(BLK)[--StringLength(BLK)] = '\0'; \
} \
StringBuffRW(BLK)[--StringLength(BLK)] = '\0'; \
} while (0)
/** Terminate on allocation failure. The String API returns void throughout, so
there is no channel to report it on, and continuing would hand the caller a
NULL buffer to write into. **/
HTS_STATIC void StringOom_(size_t size) {
fprintf(stderr, "String: out of memory allocating %lu bytes\n",
(unsigned long) size);
fflush(stderr); /* abort() flushes nothing; Windows buffers a piped stderr */
abort();
}
/** What to do when an allocation of SIZE bytes fails; overridable. **/
#ifndef STRING_OOM
#define STRING_OOM(SIZE) StringOom_(SIZE)
#endif
/** Grow so capacity_ >= CAPACITY (total bytes, including the NUL). May realloc
(invalidating prior buffer pointers); aborts on OOM. Never shrinks. **/
(invalidating prior buffer pointers); aborts via STRING_ASSERT on OOM.
Never shrinks. **/
#define StringRoomTotal(BLK, CAPACITY) \
do { \
const size_t capacity_ = (size_t) (CAPACITY); \
while ((BLK).capacity_ < capacity_) { \
const size_t newcap_ = (BLK).capacity_ < 16 ? 16 : (BLK).capacity_ * 2; \
char *const buff_ = STRING_REALLOC((BLK).buffer_, newcap_); \
\
if (buff_ == NULL) { \
STRING_OOM(newcap_); \
if ((BLK).capacity_ < 16) { \
(BLK).capacity_ = 16; \
} else { \
(BLK).capacity_ *= 2; \
} \
(BLK).buffer_ = buff_; \
(BLK).capacity_ = newcap_; \
(BLK).buffer_ = STRING_REALLOC((BLK).buffer_, (BLK).capacity_); \
STRING_ASSERT((BLK).buffer_ != NULL); \
} \
} while (0)
@@ -181,46 +165,6 @@ HTS_STATIC char *StringBuffN_(String *blk, int size) {
return StringBuffRW(*blk);
}
/** Ceiling on the pre-C99 doubling search below; BLK is emptied past it. **/
#define STRING_SPRINTF_MAX ((size_t) 16 * 1024 * 1024)
/** Replace BLK's contents with the formatted output, growing to fit, so no
fixed reserve has to bound an argument carrying remote input. An argument
may not point into BLK's own buffer, which is reallocated here.
Leaves BLK empty if the output cannot be produced (a conversion error, or
past STRING_SPRINTF_MAX): callers must not assume a non-empty result. **/
#define StringSprintf(BLK, ...) StringSprintf_(&(BLK), __VA_ARGS__)
HTS_STATIC HTS_PRINTF_FUN(2, 3) void StringSprintf_(String *blk,
const char *fmt, ...) {
size_t capacity = StringCapacity(*blk) > 256 ? StringCapacity(*blk) : 256;
for (;;) {
va_list args;
int ret;
StringRoomTotal(*blk, capacity);
va_start(args, fmt);
ret = vsnprintf(StringBuffRW(*blk), capacity, fmt, args);
va_end(args);
if (ret >= 0 && (size_t) ret < capacity) {
StringBuffRW(*blk)[ret] = '\0';
StringLength(*blk) = (size_t) ret;
return;
}
if (ret >= 0) {
capacity = (size_t) ret + 1; /* C99 said what it needs */
} else if (capacity < STRING_SPRINTF_MAX) {
capacity *= 2; /* pre-C99 msvcrt only says "too small" */
} else {
/* a conversion error returns -1 too, and no capacity ever fixes that */
StringBuffRW(*blk)[0] = '\0';
StringLength(*blk) = 0;
return;
}
}
}
/** Zero the fields (NULL buffer, no allocation). Use on an uninitialized
String only; does NOT free an existing buffer (use StringFree to reset
an owned one), so calling it on a live String leaks. **/

View File

@@ -548,37 +548,29 @@ static void proxytrack_add_DAV_Item(String * item, String * buff,
name = "Default Document for the Folder";
}
/* The path lands here twice and escapexml() expands '&' fivefold, so no
fixed reserve bounds it (#836). */
StringSprintf(
*item,
"<response xmlns=\"DAV:\">\r\n"
"<href>/webdav%s%s</href>\r\n"
"<propstat>\r\n"
"<prop>\r\n"
"<displayname>%s</displayname>\r\n"
"<iscollection>%d</iscollection>\r\n"
"<haschildren>%d</haschildren>\r\n"
"<isfolder>%d</isfolder>\r\n"
"<resourcetype>%s</resourcetype>\r\n"
"<creationdate>%d-%02d-%02dT%02d:%02d:%02dZ</creationdate>\r\n"
"<getlastmodified>%s</getlastmodified>\r\n"
"<supportedlock></supportedlock>\r\n"
"<lockdiscovery/>\r\n"
"<getcontenttype>%s</getcontenttype>\r\n"
"<getcontentlength>%d</getcontentlength>\r\n"
"<isroot>%d</isroot>\r\n"
"</prop>\r\n"
"<status>HTTP/1.1 200 OK</status>\r\n"
"</propstat>\r\n"
"</response>\r\n",
/* */
(StringBuff(*buff)[0] == '/') ? "" : "/", StringBuff(*buff), name,
isDir ? 1 : 0, isDir ? 1 : 0, isDir ? 1 : 0,
isDir ? "<collection/>" : "", timetm->tm_year + 1900,
timetm->tm_mon + 1, timetm->tm_mday, timetm->tm_hour, timetm->tm_min,
timetm->tm_sec, tms, isDir ? "httpd/unix-directory" : mime, (int) size,
isRoot ? 1 : 0);
StringRoom(*item, 1024);
sprintf(StringBuffRW(*item),
"<response xmlns=\"DAV:\">\r\n" "<href>/webdav%s%s</href>\r\n"
"<propstat>\r\n" "<prop>\r\n" "<displayname>%s</displayname>\r\n"
"<iscollection>%d</iscollection>\r\n"
"<haschildren>%d</haschildren>\r\n" "<isfolder>%d</isfolder>\r\n"
"<resourcetype>%s</resourcetype>\r\n"
"<creationdate>%d-%02d-%02dT%02d:%02d:%02dZ</creationdate>\r\n"
"<getlastmodified>%s</getlastmodified>\r\n"
"<supportedlock></supportedlock>\r\n" "<lockdiscovery/>\r\n"
"<getcontenttype>%s</getcontenttype>\r\n"
"<getcontentlength>%d</getcontentlength>\r\n"
"<isroot>%d</isroot>\r\n" "</prop>\r\n"
"<status>HTTP/1.1 200 OK</status>\r\n" "</propstat>\r\n"
"</response>\r\n",
/* */
(StringBuff(*buff)[0] == '/') ? "" : "/", StringBuff(*buff), name,
isDir ? 1 : 0, isDir ? 1 : 0, isDir ? 1 : 0,
isDir ? "<collection/>" : "", timetm->tm_year + 1900,
timetm->tm_mon + 1, timetm->tm_mday, timetm->tm_hour,
timetm->tm_min, timetm->tm_sec, tms,
isDir ? "httpd/unix-directory" : mime, (int) size, isRoot ? 1 : 0);
StringLength(*item) = (int) strlen(StringBuff(*item));
}
}
@@ -729,8 +721,11 @@ static PT_Element proxytrack_process_DAV_Request(PT_Indexes indexes,
}
/* Form response */
StringSprintf(response, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"
"<multistatus xmlns=\"DAV:\">\r\n");
StringRoom(response, 1024);
sprintf(StringBuffRW(response),
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"
"<multistatus xmlns=\"DAV:\">\r\n");
StringLength(response) = (int) strlen(StringBuff(response));
/* */
/* Root */
@@ -744,6 +739,7 @@ static PT_Element proxytrack_process_DAV_Request(PT_Indexes indexes,
if (depth > 0) {
time_t timestampRep = (time_t) - 1;
const char *prefix = StringBuff(url);
unsigned int prefixLen = (unsigned int) strlen(prefix);
char **list = PT_Enumerate(indexes, prefix, 0);
if (list != NULL) {
@@ -756,13 +752,15 @@ static PT_Element proxytrack_process_DAV_Request(PT_Indexes indexes,
int thisIsDir = (hts_lastchar(thisUrl) == '/') ? 1 : 0;
/* Item URL */
StringSprintf(itemUrl, "/%s/%s", prefix, thisUrl);
if (!StringNotEmpty(itemUrl)) { /* formatting gave up: unnameable */
continue;
}
if (thisIsDir) { /* drop the trailing '/' */
StringPopRight(itemUrl);
}
StringRoom(itemUrl,
thisUrlLen + prefixLen + sizeof("/webdav/") + 1);
StringClear(itemUrl);
sprintf(StringBuffRW(itemUrl), "/%s/%s", prefix, thisUrl);
if (!thisIsDir)
StringLength(itemUrl) = (int) strlen(StringBuff(itemUrl));
else
StringLength(itemUrl) = (int) strlen(StringBuff(itemUrl)) - 1;
StringBuffRW(itemUrl)[StringLength(itemUrl)] = '\0';
if (thisIsDir == isDir) {
size_t size = 0;
@@ -829,8 +827,6 @@ static PT_Element proxytrack_process_DAV_Request(PT_Indexes indexes,
strcpybuff(elt->msg, "Multi-Status");
StringFree(response);
fprintf(stderr, "RESPONSE:\n%s\n", elt->adr);
return elt;
}
return NULL;
@@ -1039,19 +1035,17 @@ static void proxytrack_process_HTTP(PT_Indexes indexes, T_SOC soc_c) {
const char *options = "GET, HEAD, OPTIONS, POST, PROPFIND, TRACE" ", MKCOL, DELETE, PUT"; /* Not supported */
msgCode = HTTP_OK;
StringSprintf(headers,
"HTTP/1.1 %d %s\r\n"
"DAV: 1, 2\r\n"
"MS-Author-Via: DAV\r\n"
"Cache-Control: private\r\n"
"Allow: %s\r\n",
msgCode, GetHttpMessage(msgCode), options);
StringRoom(headers, 8192);
sprintf(StringBuffRW(headers),
"HTTP/1.1 %d %s\r\n" "DAV: 1, 2\r\n" "MS-Author-Via: DAV\r\n"
"Cache-Control: private\r\n" "Allow: %s\r\n", msgCode,
GetHttpMessage(msgCode), options);
StringLength(headers) = (int) strlen(StringBuff(headers));
} else if (strcasecmp(command, "propfind") == 0) {
if (davDepth > 1) {
msgCode = 403;
msgError = "DAV Depth Limit Forbidden";
} else {
fprintf(stderr, "DEBUG: DAV-DATA=<%s>\n", StringBuff(davRequest));
listRequest = 2; /* propfind */
}
} else if (strcasecmp(command, "mkcol") == 0
@@ -1144,9 +1138,11 @@ static void proxytrack_process_HTTP(PT_Indexes indexes, T_SOC soc_c) {
proxytrack_process_DAV_Request(indexes, StringBuff(url),
davDepth)) != NULL) {
msgCode = element->statuscode;
StringSprintf(davHeaders, "DAV: 1, 2\r\n"
"MS-Author-Via: DAV\r\n"
"Cache-Control: private\r\n");
StringRoom(davHeaders, 1024);
sprintf(StringBuffRW(davHeaders),
"DAV: 1, 2\r\n" "MS-Author-Via: DAV\r\n"
"Cache-Control: private\r\n");
StringLength(davHeaders) = (int) strlen(StringBuff(davHeaders));
}
}
#endif
@@ -1165,46 +1161,45 @@ static void proxytrack_process_HTTP(PT_Indexes indexes, T_SOC soc_c) {
}
}
if (element != NULL) {
/* lifted out of the format: a directive inside a macro argument list
is undefined, and MSVC rejects it */
#ifndef NO_WEBDAV
const char *const davPart = StringBuff(davHeaders);
#else
const char *const davPart = "";
#endif
msgCode = element->statuscode;
StringSprintf(
headers,
"HTTP/1.1 %d %s\r\n"
"%s"
"Content-Type: %s%s%s%s\r\n"
"%s%s%s"
"%s%s%s"
"%s%s%s",
/* */
msgCode, element->msg, davPart,
/* Content-type: foo; [ charset=bar ] */
hts_effective_mime(element->contenttype),
((element->charset[0]) ? "; charset=\"" : ""), element->charset,
((element->charset[0]) ? "\"" : ""),
/* location */
((element->location != NULL && element->location[0])
? "Location: "
: ""),
((element->location != NULL && element->location[0])
? element->location
: ""),
((element->location != NULL && element->location[0]) ? "\r\n"
: ""),
/* last-modified */
((element->lastmodified[0]) ? "Last-Modified: " : ""),
((element->lastmodified[0]) ? element->lastmodified : ""),
((element->lastmodified[0]) ? "\r\n" : ""),
/* etag */
((element->etag[0]) ? "ETag: " : ""),
((element->etag[0]) ? element->etag : ""),
((element->etag[0]) ? "\r\n" : ""));
StringRoom(headers, 8192);
sprintf(StringBuffRW(headers),
"HTTP/1.1 %d %s\r\n"
#ifndef NO_WEBDAV
"%s"
#endif
"Content-Type: %s%s%s%s\r\n"
"%s%s%s"
"%s%s%s"
"%s%s%s",
/* */
msgCode, element->msg,
#ifndef NO_WEBDAV
/* DAV */
StringBuff(davHeaders),
#endif
/* Content-type: foo; [ charset=bar ] */
hts_effective_mime(element->contenttype),
((element->charset[0]) ? "; charset=\"" : ""),
element->charset, ((element->charset[0]) ? "\"" : ""),
/* location */
((element->location != NULL && element->location[0])
? "Location: "
: ""),
((element->location != NULL && element->location[0])
? element->location
: ""),
((element->location != NULL && element->location[0]) ? "\r\n"
: ""),
/* last-modified */
((element->lastmodified[0]) ? "Last-Modified: " : ""),
((element->lastmodified[0]) ? element->lastmodified : ""),
((element->lastmodified[0]) ? "\r\n" : ""),
/* etag */
((element->etag[0]) ? "ETag: " : ""),
((element->etag[0]) ? element->etag : ""),
((element->etag[0]) ? "\r\n" : ""));
StringLength(headers) = (int) strlen(StringBuff(headers));
} else {
/* No query string, no ending / : check the the <url>/ page */
if (StringLength(url) > 0
@@ -1214,32 +1209,29 @@ static void proxytrack_process_HTTP(PT_Indexes indexes, T_SOC soc_c) {
StringCat(urlRedirect, "/");
if (PT_LookupIndex(indexes, StringBuff(urlRedirect))) {
msgCode = 301; /* Moved Permanently */
StringSprintf(headers,
"HTTP/1.1 %d %s\r\n"
"Content-Type: text/html\r\n"
"Location: %s\r\n",
/* */
msgCode, GetHttpMessage(msgCode),
StringBuff(urlRedirect));
StringRoom(headers, 8192);
sprintf(StringBuffRW(headers),
"HTTP/1.1 %d %s\r\n" "Content-Type: text/html\r\n"
"Location: %s\r\n",
/* */
msgCode, GetHttpMessage(msgCode), StringBuff(urlRedirect)
);
StringLength(headers) = (int) strlen(StringBuff(headers));
/* */
StringSprintf(
output,
"<html"
">" PROXYTRACK_COMMENT_HEADER DISABLE_IE_FRIENDLY_HTTP_ERROR_MESSAGES
"<head>"
"<title>ProxyTrack - Page has moved</title>"
"</head>\r\n"
"<body>"
"<h3>The correct location is:</h3><br />"
"<b><a href=\"%s\">%s</a></b><br />"
"<br />"
"<br />\r\n"
"<i>Generated by ProxyTrack " PROXYTRACK_VERSION
", (C) Xavier Roche and other contributors</i>"
"\r\n"
"</body>"
"</header>",
StringBuff(urlRedirect), StringBuff(urlRedirect));
StringRoom(output,
1024 + sizeof(PROXYTRACK_COMMENT_HEADER) +
sizeof(DISABLE_IE_FRIENDLY_HTTP_ERROR_MESSAGES));
sprintf(StringBuffRW(output),
"<html>" PROXYTRACK_COMMENT_HEADER
DISABLE_IE_FRIENDLY_HTTP_ERROR_MESSAGES "<head>"
"<title>ProxyTrack - Page has moved</title>" "</head>\r\n"
"<body>" "<h3>The correct location is:</h3><br />"
"<b><a href=\"%s\">%s</a></b><br />" "<br />" "<br />\r\n"
"<i>Generated by ProxyTrack " PROXYTRACK_VERSION
", (C) Xavier Roche and other contributors</i>" "\r\n"
"</body>" "</header>", StringBuff(urlRedirect),
StringBuff(urlRedirect));
StringLength(output) = (int) strlen(StringBuff(output));
}
}
if (msgCode == 0) {
@@ -1260,29 +1252,25 @@ static void proxytrack_process_HTTP(PT_Indexes indexes, T_SOC soc_c) {
} else if (msgError == NULL) {
msgError = GetHttpMessage(msgCode);
}
StringSprintf(headers,
"HTTP/1.1 %d %s\r\n"
"Content-type: text/html\r\n",
msgCode, msgError);
StringSprintf(
output,
"<html"
">" PROXYTRACK_COMMENT_HEADER DISABLE_IE_FRIENDLY_HTTP_ERROR_MESSAGES
"<head>"
"<title>ProxyTrack - HTTP Proxy Error %d</title>"
"</head>\r\n"
"<body>"
"<h3>A proxy error has occurred while processing the "
"request.</h3><br />"
"<b>Error HTTP %d: <i>%s</i></b><br />"
"<br />"
"<br />\r\n"
"<i>Generated by ProxyTrack " PROXYTRACK_VERSION
", (C) Xavier Roche and other contributors</i>"
"\r\n"
"</body>"
"</html>",
msgCode, msgCode, msgError);
StringRoom(headers, 256);
sprintf(StringBuffRW(headers),
"HTTP/1.1 %d %s\r\n" "Content-type: text/html\r\n", msgCode,
msgError);
StringLength(headers) = (int) strlen(StringBuff(headers));
StringRoom(output,
1024 + sizeof(PROXYTRACK_COMMENT_HEADER) +
sizeof(DISABLE_IE_FRIENDLY_HTTP_ERROR_MESSAGES));
sprintf(StringBuffRW(output),
"<html>" PROXYTRACK_COMMENT_HEADER
DISABLE_IE_FRIENDLY_HTTP_ERROR_MESSAGES "<head>"
"<title>ProxyTrack - HTTP Proxy Error %d</title>" "</head>\r\n"
"<body>"
"<h3>A proxy error has occurred while processing the request.</h3><br />"
"<b>Error HTTP %d: <i>%s</i></b><br />" "<br />" "<br />\r\n"
"<i>Generated by ProxyTrack " PROXYTRACK_VERSION
", (C) Xavier Roche and other contributors</i>" "\r\n" "</body>"
"</html>", msgCode, msgCode, msgError);
StringLength(output) = (int) strlen(StringBuff(output));
}
{
char tmp[20 + 1]; /* 2^64 = 18446744073709551616 */

View File

@@ -50,7 +50,31 @@ freeport() {
proxyport=$(freeport)
icpport=$(freeport)
proxytrack "127.0.0.1:$proxyport" "127.0.0.1:$icpport" "$dir/in.arc" >"$dir/pt.log" 2>&1 &
: >"$dir/pt.log" # the drainer below creates it asynchronously
# stdout to a file is fully buffered and SIGTERM never flushes it, so a leak on
# stdout would be lost; a pty line-buffers it. The exec keeps $! on proxytrack.
"$python" -c '
import os, pty, sys
master, slave = pty.openpty()
if os.fork() == 0:
os.close(slave)
with open(sys.argv[1], "wb", 0) as log:
while True:
try:
data = os.read(master, 65536)
except OSError:
break
if not data:
break
log.write(data)
os._exit(0)
os.close(master)
os.dup2(slave, 1)
os.dup2(slave, 2)
if slave > 2:
os.close(slave)
os.execvp(sys.argv[2], sys.argv[2:])
' "$dir/pt.log" proxytrack "127.0.0.1:$proxyport" "127.0.0.1:$icpport" "$dir/in.arc" &
ptpid=$!
waited=0
until grep -qE "HTTP Proxy installed on|Unable to (initialize a temporary server|create the server)" "$dir/pt.log"; do
@@ -73,8 +97,10 @@ grep -q "HTTP Proxy installed on" "$dir/pt.log" || {
}
propfind() {
curl -s -i --max-time 30 -X PROPFIND -H "Depth: 1" \
"http://127.0.0.1:$proxyport/webdav/example.com/$1"
local path=$1
shift
curl -s -i --max-time 30 -X PROPFIND -H "Depth: 1" "$@" \
"http://127.0.0.1:$proxyport/webdav/example.com/$path"
}
resp=$(propfind page.html) || {
@@ -118,4 +144,44 @@ grep -q '^HTTP/1\.[01] 207 ' <<<"$resp" || {
exit 1
}
# Leftover traces echoed the client's body and the generated response (#911).
# The padding spills the canary past any stdio buffer, newline or not.
sentinel=PT911-DAV-BODY-CANARY
pad=$(printf '%8192s' '')
resp=$(propfind "" -H 'Expect:' --data-binary "$sentinel${pad// /x}") || {
echo "FAIL: proxytrack died serving a PROPFIND carrying a body"
cat "$dir/pt.log"
exit 1
}
grep -q '^HTTP/1\.[01] 207 ' <<<"$resp" || {
echo "FAIL: expected 207 Multi-Status on the canary listing"
printf '%s\n' "$resp"
exit 1
}
# The 207 is what makes the absence below mean something: a depth-rejected
# PROPFIND is logged 403 without ever reaching the traces. It is logged after
# them, so waiting on it also fences the drainer's copy.
waited=0
until test "$(grep -ci ' \* log: HTTP [^ ]* 207 [0-9]* propfind ' "$dir/pt.log" || true)" -ge 3; do
test "$waited" -lt 50 || {
echo "FAIL: fewer than three PROPFINDs were answered 207, so the checks below prove nothing"
cat "$dir/pt.log"
exit 1
}
sleep 0.1
waited=$((waited + 1))
done
# Neither what the client sent nor the generated body is echoed anywhere; the
# two literals name the exact regression on top.
log=$(<"$dir/pt.log")
for marker in "$sentinel" 'Default Document for the Folder' 'DEBUG: DAV-DATA' '^RESPONSE:'; do
if grep -q "$marker" <<<"$log"; then
echo "FAIL: a PROPFIND echoed '$marker' into proxytrack's output"
printf '%s\n' "$log"
exit 1
fi
done
echo "OK: PROPFIND on an exact cache entry lists its default document and survives"

View File

@@ -1,162 +0,0 @@
#!/bin/bash
#
# escapexml() expands '&' fivefold, so an unauthenticated PROPFIND path of
# escaped ampersands used to overflow the fixed DAV item reserve (#836). The
# Depth: 1 listing at the end covers the enumeration branch's own item URLs.
set -euo pipefail
: "${top_srcdir:=..}"
# shellcheck source=tests/testlib.sh
. "$top_srcdir/tests/testlib.sh"
python=$(find_python) || {
echo "python3 missing, skipping"
exit 77
}
command -v curl >/dev/null 2>&1 || {
echo "curl missing, skipping"
exit 77
}
# MSYS cannot reap a native listener, and the orphan wedges the suite (#595).
if is_windows; then
echo "windows: cannot reap a backgrounded proxytrack, skipping"
exit 77
fi
dir=$(mktemp -d)
ptpid=
cleanup() {
stop_server "$ptpid"
rm -rf "$dir"
}
trap 'set +e; cleanup' EXIT
trap 'set +e; cleanup; exit 1' INT TERM HUP
printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 5\r\n\r\n' >"$dir/hdr"
printf 'hello' >"$dir/body"
alen=$(($(wc -c <"$dir/hdr") + $(wc -c <"$dir/body")))
{
printf 'filedesc://t.arc 0.0.0.0 20250101000000 text/plain 200 - - 0 t.arc 9\n'
printf '2 0 test\n'
printf '\n\n'
printf 'http://example.com/page.html 0.0.0.0 20250101000000 text/html 200 - - 0 t.arc %d\n' "$alen"
cat "$dir/hdr" "$dir/body"
# a record begins on the newline following the previous one's data
printf '\n'
# gives the Depth: 1 listing below a child directory as well as a child file
printf 'http://example.com/sub/deep.html 0.0.0.0 20250101000000 text/html 200 - - 0 t.arc %d\n' "$alen"
cat "$dir/hdr" "$dir/body"
} >"$dir/in.arc"
freeport() {
"$python" -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()'
}
proxyport=$(freeport)
icpport=$(freeport)
proxytrack "127.0.0.1:$proxyport" "127.0.0.1:$icpport" "$dir/in.arc" >"$dir/pt.log" 2>&1 &
ptpid=$!
waited=0
until grep -qE "HTTP Proxy installed on|Unable to (initialize a temporary server|create the server)" "$dir/pt.log"; do
kill -0 "$ptpid" 2>/dev/null || {
echo "FAIL: proxytrack exited before listening"
cat "$dir/pt.log"
exit 1
}
test "$waited" -lt 50 || {
echo "FAIL: proxytrack never announced its listen port"
exit 1
}
sleep 0.1
waited=$((waited + 1))
done
grep -q "HTTP Proxy installed on" "$dir/pt.log" || {
echo "FAIL: proxytrack failed to bind"
cat "$dir/pt.log"
exit 1
}
propfind() {
curl -s -i --max-time 30 -X PROPFIND -H "Depth: 0" \
"http://127.0.0.1:$proxyport/webdav/x/$1"
}
# Amplification, not raw length, clears the old 1024-byte reserve: 900 '&'
# become 4500 bytes, written twice, all under the 1024-byte request-line cap.
amps=$("$python" -c "print('&' * 900)")
resp=$(propfind "$amps") || {
echo "FAIL: proxytrack died on an escapexml-amplified PROPFIND"
cat "$dir/pt.log"
exit 1
}
grep -q '^HTTP/1\.[01] 207 ' <<<"$resp" || {
echo "FAIL: expected 207 Multi-Status for the amplified path"
head -c 512 <<<"$resp"
exit 1
}
# Count what came back: a clipped path answers 207 too. The run appears in
# the href and again in the displayname.
got=$({ grep -o '&amp;' <<<"$resp" || true; } | wc -l)
test "$got" -eq 1800 || {
echo "FAIL: 900 ampersands went in, $got came back escaped, wanted 1800"
exit 1
}
# Unescaped, so the bound is shown to be on the written length, not on '&'.
plain=$("$python" -c "print('a' * 900)")
resp=$(propfind "$plain") || {
echo "FAIL: proxytrack died on a 900-byte plain PROPFIND path"
cat "$dir/pt.log"
exit 1
}
grep -q "<href>/webdav/x/$plain</href>" <<<"$resp" || {
echo "FAIL: a 900-byte path did not come back whole"
exit 1
}
# Depth: 1 is the only route into the enumeration branch, which builds each
# item URL from the prefix and the child name. A child directory is enumerated
# with a trailing '/' that has to come back off, or its name is lost and the
# entry lists as the folder's default document.
resp=$(curl -s -i --max-time 30 -X PROPFIND -H "Depth: 1" \
"http://127.0.0.1:$proxyport/webdav/example.com/") || {
echo "FAIL: proxytrack died on a Depth: 1 listing"
cat "$dir/pt.log"
exit 1
}
grep -q '^HTTP/1\.[01] 207 ' <<<"$resp" || {
echo "FAIL: expected 207 Multi-Status for the Depth: 1 listing"
printf '%s\n' "$resp"
exit 1
}
for want in '<href>/webdav/example\.com</href>' \
'<href>/webdav/example\.com/page\.html</href>' \
'<href>/webdav/example\.com/sub</href>' \
'<displayname>sub</displayname>'; do
grep -q "$want" <<<"$resp" || {
echo "FAIL: the Depth: 1 listing is missing $want"
printf '%s\n' "$resp"
exit 1
}
done
responses=$(grep -c '<response ' <<<"$resp" || true)
test "$responses" -eq 3 || {
echo "FAIL: expected the root, the file and the directory, got $responses responses"
printf '%s\n' "$resp"
exit 1
}
# The overflow killed the listener, so a later request is the liveness proof.
resp=$(propfind "") || {
echo "FAIL: proxytrack died before the follow-up listing"
cat "$dir/pt.log"
exit 1
}
grep -q '^HTTP/1\.[01] 207 ' <<<"$resp" || {
echo "FAIL: expected 207 Multi-Status on the follow-up listing"
printf '%s\n' "$resp"
exit 1
}
echo "OK: an escapexml-amplified PROPFIND path is served whole and does not overflow"

View File

@@ -1,30 +0,0 @@
#!/bin/bash
#
# A trailer section after the terminating chunk (RFC 9112 7.1.2, #855) is
# discarded, bounded by HTS_LINE_BLOCK_SIZE (huge.html), and junk where a data
# chunk's own CRLF belongs stays a framing error (bogus.html). eof.html pins the
# deliberate leniency: past a complete body, an unterminated section still lands.
set -euo pipefail
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" \
--log-not-found 'Invalid chunk.*chunktrail/(one|many|none|file|eof)' \
--log-not-found 'illegal chunk CRLF.*chunktrail/(one|many|none|file|huge|eof)' \
--file-matches 'chunktrail/one.html' 'CHUNKTRAIL-ONE' \
--file-matches 'chunktrail/many.html' 'CHUNKTRAIL-MANY' \
--file-matches 'chunktrail/none.html' 'CHUNKTRAIL-NONE' \
--file-matches 'chunktrail/file.bin' 'CHUNKTRAIL-BIN' \
--file-min-bytes 'chunktrail/file.bin' 16384 \
--file-matches 'chunktrail/eof.html' 'CHUNKTRAIL-EOF' \
--cache-found '/chunktrail/one.html' \
--cache-found '/chunktrail/many.html' \
--cache-found '/chunktrail/file.bin' \
--log-found 'Interrupted transfer.*chunktrail/huge\.html' \
--not-found 'chunktrail/huge.html' \
--cache-not-found '/chunktrail/huge.html' \
--log-found 'illegal chunk CRLF.*chunktrail/bogus\.html' \
--not-found 'chunktrail/bogus.html' \
--cache-not-found '/chunktrail/bogus.html' \
httrack 'BASEURL/chunktrail/index.html'

View File

@@ -1,25 +0,0 @@
#!/bin/bash
#
# StringSprintf writes the terminator at buffer[ret], so a `ret <= capacity`
# guard overflows by one byte whenever the output exactly fills the capacity
# (#836). The proxytrack crawl tests never land on a capacity boundary.
set -euo pipefail
: "${top_srcdir:=..}"
# Resolve httrack before any cd: PATH may carry a build-relative entry.
bin=$(command -v httrack) || {
echo "FAIL: httrack not found on PATH" >&2
exit 1
}
fail() {
echo "FAIL: $1" >&2
exit 1
}
out=$("$bin" -#test=strsprintf) || fail "httrack -#test=strsprintf exited non-zero: $out"
test "$out" = "strsprintf self-test: OK" || fail "expected 'strsprintf self-test: OK', got: $out"
exit 0

View File

@@ -1,53 +0,0 @@
#!/bin/bash
#
# A String that cannot grow must stop the process loudly rather than carry on
# with a NULL buffer and a capacity already bumped. assert() is compiled out of
# the MSVC Release build, so the check cannot lean on it (#915). The failure is
# injected through a realloc stub: asking a real allocator for a size it should
# refuse is a guess about the machine, not a test.
set -euo pipefail
ulimit -c 0 # a deliberate abort must not litter the box with cores
fail() {
echo "FAIL: $1" >&2
exit 1
}
# Unset means this is not an automake run (the Windows job runs the scripts
# directly), so the helper does not exist. Set but missing is a build problem,
# not a skip, or the assertions below would pass vacuously.
if [ -z "${STRINGOOM_BIN:-}" ]; then
echo "STRINGOOM_BIN unset, no automake environment, skipping" >&2
exit 77
fi
[ -r "$STRINGOOM_BIN" ] || fail "$STRINGOOM_BIN was not built"
# Control first: with the stub allocating for real, growth must not reach the
# failure path at all.
out=$("$STRINGOOM_BIN" grow) || fail "grow case exited non-zero: $out"
test "$out" = "grow: OK" || fail "expected 'grow: OK', got: $out"
# A failed realloc must reach the handler with the size it asked for, and leave
# the String untouched rather than half-grown.
out=$("$STRINGOOM_BIN" hook) || fail "hook case exited non-zero: $out"
test "$out" = "hook: OK" || fail "expected 'hook: OK', got: $out"
# Same on a String that already owns a buffer: the live pointer must survive.
out=$("$STRINGOOM_BIN" keep) || fail "keep case exited non-zero: $out"
test "$out" = "keep: OK" || fail "expected 'keep: OK', got: $out"
# The shipped handler prints and aborts, so don't let set -e trip on it.
err=$("$STRINGOOM_BIN" abort 2>&1) || true
case "$err" in
*"NOT aborted"*)
fail "the failed allocation was not caught: $err"
;;
*"String: out of memory"*) ;;
*)
fail "expected the String out-of-memory abort, got: $err"
;;
esac
exit 0

View File

@@ -36,7 +36,6 @@ TESTS_ENVIRONMENT += THREADATTRFAIL_LA=$(abs_builddir)/libthreadattrfail.la
TESTS_ENVIRONMENT += THREADATTRFAIL_LIB=$(abs_builddir)/$(LT_CV_OBJDIR)/libthreadattrfail.so
TESTS_ENVIRONMENT += NOBACKTRACE_LA=$(abs_builddir)/libnobacktrace.la
TESTS_ENVIRONMENT += NOBACKTRACE_LIB=$(abs_builddir)/$(LT_CV_OBJDIR)/libnobacktrace.so
TESTS_ENVIRONMENT += STRINGOOM_BIN=$(abs_builddir)/stringoom$(EXEEXT)
# rename() interposer for 01_engine-renameover.test; -rpath is what makes
# libtool build a shared module rather than a static-only convenience library.
@@ -54,12 +53,6 @@ libthreadattrfail_la_LIBADD = $(DL_LIBS)
libnobacktrace_la_SOURCES = nobacktrace.c
libnobacktrace_la_LDFLAGS = -module -avoid-version -rpath $(abs_builddir)
# realloc stub for 152_engine-string-oom.test; links nothing, so it stays a
# plain binary rather than a libtool wrapper script.
check_PROGRAMS = stringoom
stringoom_SOURCES = stringoom.c
stringoom_CPPFLAGS = -I$(top_srcdir)/src
TEST_EXTENSIONS = .test
# Run each .test through bash instead of execve()ing it. This lets "make check"
# work when the source tree sits on a noexec filesystem (the driver would

View File

@@ -1692,112 +1692,6 @@ class Handler(SimpleHTTPRequestHandler):
pass
self.close_connection = True
# #855: a trailer section after the terminating zero-length chunk.
def send_chunked_trailed(self, body, trailers, ctype="text/html; charset=utf-8"):
self.protocol_version = "HTTP/1.1"
self.send_response(200)
self.send_header("Content-Type", ctype)
self.send_header("Transfer-Encoding", "chunked")
self.send_header("Connection", "close")
self.end_headers()
if self.command == "HEAD":
return
try:
half = len(body) // 2
for piece in (body[:half], body[half:]):
self.wfile.write(b"%X\r\n" % len(piece) + piece + b"\r\n")
self.wfile.write(
b"0\r\n" + b"".join(t + b"\r\n" for t in trailers) + b"\r\n"
)
self.wfile.flush()
except OSError:
pass
self.close_connection = True
def route_chunktrail_index(self):
self.send_html(
'\t<a href="one.html">one</a>\n'
'\t<a href="many.html">many</a>\n'
'\t<a href="none.html">none</a>\n'
'\t<a href="huge.html">huge</a>\n'
'\t<a href="bogus.html">bogus</a>\n'
'\t<a href="eof.html">eof</a>\n'
'\t<a href="file.bin">file</a>\n'
)
def route_chunktrail_one(self):
self.send_chunked_trailed(
b"<html><body><p>CHUNKTRAIL-ONE</p></body></html>", [b"X-Foo: bar"]
)
# Spans several reads: longer than the reader's 256 bytes per call.
def route_chunktrail_many(self):
self.send_chunked_trailed(
b"<html><body><p>CHUNKTRAIL-MANY</p></body></html>",
[b"X-Field-%02d: %s" % (n, b"v" * 40) for n in range(20)],
)
# Control: same body, no trailer section.
def route_chunktrail_none(self):
self.send_chunked_trailed(
b"<html><body><p>CHUNKTRAIL-NONE</p></body></html>", []
)
# Past HTS_LINE_BLOCK_SIZE: the discard stays bounded, so the transfer drops.
def route_chunktrail_huge(self):
self.send_chunked_trailed(
b"<html><body><p>CHUNKTRAIL-HUGE</p></body></html>",
[b"X-Bloat-%04d: %s" % (n, b"w" * 100) for n in range(160)],
)
# Only the terminating chunk opens the section, so junk in a data chunk's
# own CRLF stays a framing error.
def route_chunktrail_bogus(self):
self.protocol_version = "HTTP/1.1"
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Transfer-Encoding", "chunked")
self.send_header("Connection", "close")
self.end_headers()
if self.command == "HEAD":
return
try:
body = b"<html><body><p>CHUNKTRAIL-BOGUS</p></body></html>"
self.wfile.write(b"%X\r\n" % len(body) + body + b"X-Foo: bar\r\n")
self.wfile.write(b"0\r\n\r\n")
self.wfile.flush()
except OSError:
pass
self.close_connection = True
# Body complete, then EOF before the blank line closing the section. The
# terminating chunk already arrived, so the payload stands.
def route_chunktrail_eof(self):
self.protocol_version = "HTTP/1.1"
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Transfer-Encoding", "chunked")
self.send_header("Connection", "close")
self.end_headers()
if self.command == "HEAD":
return
try:
body = b"<html><body><p>CHUNKTRAIL-EOF</p></body></html>"
self.wfile.write(b"%X\r\n" % len(body) + body + b"\r\n")
self.wfile.write(b"0\r\nX-Checksum: deadbeef\r\n")
self.wfile.flush()
except OSError:
pass
self.close_connection = True
# The direct-to-disk path, where the body never sits in memory.
def route_chunktrail_file(self):
self.send_chunked_trailed(
b"CHUNKTRAIL-BIN\n" + b"\x41\x42\x43\xfd" * 4096,
[b"X-Checksum: 0badc0de", b"X-Done: 1"],
"application/octet-stream",
)
# Aborts the chunked body with an RST, so the read fails rather than seeing a
# clean EOF and the transfer is already in error before the framing check.
def route_chunktrunc_reset(self):
@@ -2444,14 +2338,6 @@ class Handler(SimpleHTTPRequestHandler):
"/chunktrunc/stay.html": route_chunktrunc_stay,
"/chunktrunc/hostile.html": route_chunktrunc_hostile,
"/chunktrunc/reset.bin": route_chunktrunc_reset,
"/chunktrail/index.html": route_chunktrail_index,
"/chunktrail/one.html": route_chunktrail_one,
"/chunktrail/many.html": route_chunktrail_many,
"/chunktrail/none.html": route_chunktrail_none,
"/chunktrail/huge.html": route_chunktrail_huge,
"/chunktrail/bogus.html": route_chunktrail_bogus,
"/chunktrail/eof.html": route_chunktrail_eof,
"/chunktrail/file.bin": route_chunktrail_file,
"/errpage/index.html": route_errpage_index,
"/errpage/good.html": route_errpage_good,
"/errpage/missing.html": route_errpage_missing,

View File

@@ -1,157 +0,0 @@
/* ------------------------------------------------------------ */
/*
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 1998 Xavier Roche and other contributors
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Please visit our Website: http://www.httrack.com
*/
/* Drives StringRoomTotal's allocation-failure path through a realloc stub:
asking a real allocator for a size it should refuse is a guess about the
machine, not a test (#915). One case per run, named by argv[1]. */
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int alloc_fails = 0;
static int oom_jumps = 0;
static int oom_calls = 0;
static size_t oom_size = 0;
static jmp_buf oom_jump;
static char *test_realloc(char *buff, size_t size) {
if (alloc_fails) {
return NULL;
}
return (char *) realloc(buff, size);
}
/* Declared before the include: StringBuffN_ and StringSprintf_ expand
STRING_OOM inside the header itself. */
static void test_oom(size_t size);
#define STRING_REALLOC(BUFF, SIZE) test_realloc(BUFF, SIZE)
#define STRING_FREE(BUFF) free(BUFF)
#define STRING_OOM(SIZE) test_oom(SIZE)
#include "htsstrings.h"
/* Either returns to the caller, leaving the String observable, or runs the
shipped handler. */
static void test_oom(size_t size) {
oom_calls++;
oom_size = size;
if (oom_jumps) {
longjmp(oom_jump, 1);
}
StringOom_(size);
}
/* File scope, so longjmp cannot leave them indeterminate. */
static String room = STRING_EMPTY;
static const char *kept = NULL;
/* Control: with the stub allocating for real, nothing must reach the handler.
Without this, a stub stuck in failing mode would "prove" every case. */
static int grow_case(void) {
StringRoomTotal(room, 100);
if (oom_calls != 0) {
printf("grow: FAIL (handler ran %d times)\n", oom_calls);
return 1;
}
if (StringBuff(room) == NULL || StringCapacity(room) < 100) {
printf("grow: FAIL (capacity %u)\n", (unsigned) StringCapacity(room));
return 1;
}
StringFree(room);
printf("grow: OK\n");
return 0;
}
static int hook_case(void) {
alloc_fails = oom_jumps = 1;
if (setjmp(oom_jump) == 0) {
StringRoomTotal(room, 100);
printf("hook: FAIL (grew through a failing allocator)\n");
return 1;
}
if (StringBuff(room) != NULL || StringCapacity(room) != 0 ||
StringLength(room) != 0) {
printf("hook: FAIL (capacity %u, buffer %s)\n",
(unsigned) StringCapacity(room),
StringBuff(room) == NULL ? "null" : "set");
return 1;
}
if (oom_size != 16) { /* the first doubling, and the size we report */
printf("hook: FAIL (reported %u bytes, expected 16)\n",
(unsigned) oom_size);
return 1;
}
printf("hook: OK\n");
return 0;
}
/* The one the old code got wrong: a failed realloc must not overwrite the live
buffer with NULL nor bump the capacity past what was allocated. */
static int keep_case(void) {
StringCopy(room, "abc");
kept = StringBuff(room);
alloc_fails = oom_jumps = 1;
if (setjmp(oom_jump) == 0) {
StringRoomTotal(room, 1000);
printf("keep: FAIL (grew through a failing allocator)\n");
return 1;
}
if (StringBuff(room) != kept || StringCapacity(room) != 16 ||
StringLength(room) != 3 || strcmp(StringBuff(room), "abc") != 0) {
printf("keep: FAIL (buffer %s, capacity %u)\n",
StringBuff(room) == kept ? "kept" : "moved",
(unsigned) StringCapacity(room));
return 1;
}
alloc_fails = 0;
StringFree(room);
printf("keep: OK\n");
return 0;
}
/* Runs the shipped handler, which must print and abort. */
static int abort_case(void) {
alloc_fails = 1;
StringRoomTotal(room, 100);
printf("abort: NOT aborted\n");
return 1;
}
int main(int argc, char **argv) {
const char *const mode = argc > 1 ? argv[1] : "";
if (strcmp(mode, "grow") == 0) {
return grow_case();
} else if (strcmp(mode, "hook") == 0) {
return hook_case();
} else if (strcmp(mode, "keep") == 0) {
return keep_case();
} else if (strcmp(mode, "abort") == 0) {
return abort_case();
}
fprintf(stderr, "usage: %s grow|hook|keep|abort\n", argv[0]);
return 2;
}

View File

@@ -218,8 +218,4 @@ TESTS += 143_engine-backtrace-empty.test
TESTS += 144_engine-datadir.test
TESTS += 145_webhttrack-datadir.test
TESTS += 146_bash-shell.test
TESTS += 147_local-proxytrack-webdav-overflow.test
TESTS += 149_local-chunked-trailer.test
TESTS += 148_engine-spoolname.test
TESTS += 150_engine-strsprintf.test
TESTS += 152_engine-string-oom.test