Compare commits

...

10 Commits

Author SHA1 Message Date
Xavier Roche
d9d25196bb Fix two more tests the Win32 CI leg exposed
Both are test bugs surfaced by running on 32-bit Windows for the first
time, not engine bugs:

- strsafe drove its overflow through a char[4]. On a 32-bit build that
  equals sizeof(char*), so htssafe's array-vs-pointer heuristic
  (sizeof(A) != sizeof(char*), the MSVC path) reads the array as a
  pointer and skips the bound: the copy then genuinely overflows and
  crashes instead of aborting cleanly. Size the buffer off the pointer
  width so the checked path runs everywhere.

- idna's malformed-UTF-8 rejection case passed the bad bytes through
  argv. They cannot survive a Windows command line: the UTF-16 round-trip
  turns them into valid U+FFFD, which the encoder then accepts. Feed them
  as hex so the exact bytes reach the bundled punycode encoder, which
  rejects them identically on every platform.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-14 19:08:34 +02:00
Xavier Roche
d6153cbc86 Fix the Win32 CLI build: include htscharset.h after winsock2.h
htscharset.h pulls in <windows.h>, which drags in the legacy <winsock.h>
unless <winsock2.h> was included first. In httrack.c it sat ahead of the
net headers, so the x86 build redefined every sockaddr/winsock symbol
(C2011). Move it below htslib.h, where winsock2.h is already in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-14 18:52:25 +02:00
Xavier Roche
2438f55327 Log the launch banner argv without re-encoding it
The banner ran hts_convertStringSystemToUTF8() over each argv element on
Windows, which was right when argv came in the ANSI codepage. argv is
UTF-8 on every platform now, so that pass double-encoded a non-ASCII
argument into mojibake in hts-log.txt. Log the bytes as they are.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-14 18:41:10 +02:00
Xavier Roche
0e5c2d6ee9 Run the engine test suite on Windows CI
The ~90 tests only ever ran on Linux and macOS, so a Windows-only
regression was invisible until a user hit it - and the first run of the
offline ones found a third of them failing.

Drive them from Git Bash against the native MSVC httrack.exe. Two things
that silently ruin the run: MSYS rewrites any argument shaped like a POSIX
path, and a URL path is shaped exactly like one, so "-#test=mime /a/b.html"
reached the engine as "C:/Program Files/Git/a/b.html"; and a suite that
degrades to all-skipped would report green having tested nothing, hence
the floor on tests actually passed.

This subsumes the codec and cache self-tests the workflow ran inline. The
.gitattributes pins the test scripts to LF: a converting checkout rewrites
them to CRLF and bash then dies on $'\r' on every line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-14 18:24:54 +02:00
Xavier Roche
ae0298aac6 Make the engine tests pass on Windows
Three of them could never pass there, and none of the three was an engine
bug:

- rcfile wrote .httrackrc, but HTS_HTTRACKRC is "httrackrc" on Windows,
  so the engine never found it. Write both names.
- filelist made a file unreadable with chmod 000, which Windows ignores.
  Probe whether the mode is enforced instead of assuming, which also
  subsumes the root special-case.
- ftp-line was #ifndef _WIN32 for its socketpair(), though get_ftp_line()
  itself is portable. Pair over loopback TCP and register it everywhere.

The charset self-tests fed raw non-UTF-8 bytes through argv to probe the
decoders. Those bytes cannot survive any Windows command line, whatever
argv does, so take them as hex - the convention the sniff self-test
already uses - and keep the coverage on every platform.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-14 18:24:54 +02:00
Xavier Roche
8a4ca8357a Take argv as UTF-8 on Windows, not the ANSI codepage (#573)
httrack.exe is an MBCS build, so the CRT transcodes the UTF-16 command
line down to the machine's ANSI codepage before handing us char **argv.
Non-ASCII arguments are lossy, and anything outside that codepage is
destroyed: a Cyrillic or CJK URL on a Western-codepage box becomes '?'.

Everything else in the engine already treats char* as UTF-8 on Windows -
FOPEN, STAT, UNLINK are hts_*_utf8 wrappers converting to UTF-16 at the
syscall boundary. argv is the one thing that never got the memo.

Decode the real UTF-16 command line at the entry point instead. The
manifest's activeCodePage would fix it in one line, but it needs Windows
10 1903+ and is silently ignored below that, which is precisely the
population running the codepages that mangle non-Latin URLs today.

Windows-only, so no POSIX symbol and no soname change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-14 18:24:37 +02:00
Xavier Roche
1184839060 Cap the wildcard matcher's recursion depth (#574)
strjoker() recursed once per pattern segment, bounded only by the length
cap: a hostile filter of 1023 stars reached 2046 frames, ~900KB of stack.
That fits Linux's 8MB but not the 1MB a Windows thread gets, so
-#test=filterbounds died silently on MSVC x64 and Win32 instead of
rejecting the pattern it is meant to reject.

Cap the depth at 256 (real filters use fewer than ten). A cut branch does
not memoize its failure: the same pair may still match when reached at a
shallower depth.

The self-test now asserts the depth reached equals the cap, and the .test
re-runs it under a 512K stack, which segfaults without the cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-14 18:15:26 +02:00
Xavier Roche
ee1aebd935 Harden the ProxyTrack cache readers against corrupt input (#572)
proxy/store.c carried its own copy of htscache.c's old-format (.dat/.ndx) cache readers without the bounds hardening the engine copy got in #568. cache_brstr/cache_rstr trusted a length prefix from the cache file and wrote it into fixed buffers unbounded, and cache_rint/cache_rLLint used an unchecked sscanf result as a size or offset, so a crafted cache overflowed firstline[256] and the PT_Element fields and could wrap a malloc(size+1).

Ports the engine's discipline into the proxy copy: thread the destination capacity through the readers and clamp while consuming the declared length, check every sscanf, allocate the body through a bounded helper, fix an index[index_id] off-by-one, and bound the startUrl/previous_save concatenations. Also casts the coucal void* key at the %s snprintf. The .dat/.ndx format is dead (zip only now), so this is defense-in-depth on the legacy reader; a corrupt-cache test guards it under the sanitizer CI.
2026-07-14 17:58:39 +02:00
Xavier Roche
b4957f0625 Extract the proxy tunneling code into its own module (#571)
* Extract the proxy tunneling code into htsproxy.c

htslib.c is the engine's catch-all "subroutines" file; the two proxy
tunnels living in it (HTTP CONNECT and the SOCKS5 handshake) are one
coherent thing: blocking raw-socket negotiation with the proxy, run from
the same back_wait callsite before TLS. Move both, plus proxy_getline and
the socks5 statics, into htsproxy.c/.h, which also gives the socks5_test_io
self-test seam a home away from the public-ish htslib.h.

Pure code motion: the moved bytes are identical, and hts_proxy_is_socks
stays in htslib.c next to hts_parse_proxy and jump_protocol_const, being a
URL-scheme predicate rather than handshake logic.

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

* Keep the vcxproj source list alphabetical

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 16:21:51 +02:00
Xavier Roche
c9d36c82c7 Files of 2 GB or more are mishandled on Windows, and on any 32-bit build (#568)
* Report file sizes as 64-bit, so Windows stops mishandling files past 2GB

MSVC's off_t is a 32-bit long in x64 builds as well as Win32, and struct
_stat expands to _stat64i32, whose st_size is that same 32-bit long. So
fsize(), fsize_utf8() and fpsize() truncated twice over: once in the stat,
once in their own return type. A mirrored file of 2GB or more got a wrapped,
often negative size, which fed resume/206 handling, the cache body-size
reads, and readfile2()'s allocation.

Use the 64-bit _stat64 family on Windows and return LLint from the three
size helpers, widening the callers' locals to match. INTsys and HTS_FSEEKO
now cover MSVC too, so readfile2() and fpsize() reach the _fseeki64/_ftelli64
aliases htslib.h already declared. htsback.c's two 'not (off_t)' comments
described this trap but the value feeding that path was still 32-bit.

STRUCT_STAT is in an installed header, so hts_stat_utf8()'s signature changes:
a deliberate Windows-only ABI break, agreed with Xavier. The DLL ships next to
the exe with no soname contract. POSIX is untouched, where off_t is already
64-bit under LFS.

The new -#test=fsize self-test builds a 3GB sparse file and asserts both the
reported size and the 8-byte return width. Forcing the helpers back to a
32-bit return reproduces the Windows failure exactly (-1073741824) and the
test catches it.

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

* Report errno when the fsize self-test cannot create or reopen its file

The linux i386 leg fails with 'fpsize is -1', which only says the reopen
returned NULL. Print strerror(errno) on both the create and the reopen path,
and check fclose() so a failed flush stops being silent.

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

* Size files with fseeko/ftello, never ftell: ftell is 32-bit on i386

fpsize() gated fseeko/ftello behind HTS_FSEEKO, which is defined from
'#ifdef LFS_FLAG'. LFS_FLAG is a configure make variable carrying the
-D_FILE_OFFSET_BITS=64 flags, never a C macro, so that test is dead and the
fallback ftell/fseek branch is the one every autotools build compiled. ftell
returns long: 64-bit on x86-64, which hid the bug, but 32-bit on i386, where
it fails with EOVERFLOW past 2GB and fpsize() returns -1.

Call fseeko/ftello unconditionally, as htsback.c already does; MSVC gets them
via the _fseeki64/_ftelli64 aliases in htslib.h. HTS_FSEEKO had no other user.

The dead LFS_FLAG test also leaves INTsys as int on POSIX, capping readfile2()
at 2GB. Widening it changes a typedef in an installed header, so that is left
for a separate change; the FIXME records it.

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

* Fix the INTsys widening's fallout, and make the fsize test bite

Widening INTsys to 64-bit on MSVC broke two call sites that assumed it was
an int, both found by an adversarial review of this branch:

- proxy/store.c's cache_rstr_addr() parsed it with sscanf("%d"), writing 4
  bytes into an 8-byte object and leaving the high word indeterminate; that
  value then sized a malloc() and an fread(). Its sibling cache_rstr() already
  used INTsysP. proxytrack.vcxproj builds this file, so it was live on MSVC.
- readfile2()/readfile_utf8() fed a now-64-bit length to malloct() and fread(),
  which take size_t: 32-bit on Win32 and i386. The two truncate independently,
  so a file of 4GiB-1 gave malloc(0) with a 4GiB read into it. Route the size
  through llint_to_size_t() and fail closed when it does not fit.

The self-test only exercised fsize_utf8(), so reverting fsize()'s _stat64 hunk
left it green, and fsize() is what sizes file:// bodies and --list. It now
checks both variants, the absent-file -1 contract, and uses 5GB rather than
3GB: a 32-bit truncation of 3GB is negative and obvious, of 5GB it is
1073741824, a plausible size. NTFS needs FSCTL_SET_SPARSE to leave the hole
unallocated; POSIX gives it for free.

Windows CI now runs the cache, cache-corrupt and fsize self-tests. The cache
fixes in #567 cannot be verified on POSIX at all, where fconv() is a no-op.

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

* Check the sscanf return in proxytrack's cache reader

cache_rstr() and cache_rstr_addr() declared an uninitialized INTsys and never
checked whether sscanf matched. On a truncated or malformed cache entry the
length stays indeterminate, and it then sizes an fread() and, in the addr
case, a malloc(). Initialize it and require a match, so a bad entry reads as
length 0.

Flagged by CodeQL (cpp/missing-check-scanf) once the widening touched that
line; the alert predates it on master, and its sibling had the same hole.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 15:49:59 +02:00
32 changed files with 1222 additions and 763 deletions

5
.gitattributes vendored
View File

@@ -1,3 +1,8 @@
# Resource scripts are Windows tooling input and must stay CRLF: the engine has no
# encoding guard, so an autocrlf checkout could otherwise flatten them silently.
*.rc text eol=crlf
# The test scripts run under Git Bash on the Windows CI; a CRLF checkout makes
# bash die on $'\r' on every line of them.
*.test text eol=lf
*.sh text eol=lf

View File

@@ -119,22 +119,61 @@ jobs:
throw "CRT mismatch across the boundary: libhttrack.dll=$a httrack.exe=$b"
}
# The engine self-tests exercise the codecs the DLL was linked with: a
# missing brotli/zstd decoder shows up here, not just at link time.
- name: Run the content-coding self-tests
shell: pwsh
# The engine ships ~90 tests and none had ever run on Windows: "make check"
# is Linux/macOS only. These are the offline ones, driven from Git Bash
# against the native httrack.exe. They subsume the self-tests this step
# used to run inline (codecs, cache, fsize).
- name: Run the engine test suite (offline tests)
shell: bash
working-directory: tests
run: |
$ErrorActionPreference = "Stop"
$exe = "src\${{ matrix.platform }}\${{ matrix.configuration }}\httrack.exe"
$tmp = New-Item -ItemType Directory -Path (Join-Path $env:RUNNER_TEMP "st")
foreach ($t in @("acceptencoding", "contentcodings")) {
$out = & $exe -O NUL "-#test=$t" $tmp.FullName run
if ($LASTEXITCODE -ne 0) { throw "$t failed" }
if (-not ($out | Select-String -SimpleMatch "$t self-test OK")) {
throw "$t did not report OK: $out"
}
Write-Host ($out -join "`n")
}
set -u
bin="$(cygpath -u "$GITHUB_WORKSPACE")/src/${{ matrix.platform }}/${{ matrix.configuration }}"
export PATH="$bin:$PATH"
command -v httrack >/dev/null || { echo "::error::no httrack.exe in $bin"; exit 1; }
# httrack.exe is native, so MSYS rewrites any argument shaped like a
# POSIX path, and a URL path is shaped exactly like one: "/a/b.html"
# reached the engine as "C:/Program Files/Git/a/b.html". Switch that
# off, and hand the tests a TMPDIR that is already a Windows path.
export MSYS_NO_PATHCONV=1
export MSYS2_ARG_CONV_EXCL='*'
TMPDIR="$(cygpath -m "$RUNNER_TEMP")"
export TMPDIR
pass=0 fail=0 skip=0 failed=""
for t in 00_runnable.test 01_engine-*.test 01_zlib-*.test; do
rc=0
bash "$t" >"$t.log" 2>&1 || rc=$?
case "$rc" in
0) pass=$((pass + 1)); echo "PASS $t" ;;
77) skip=$((skip + 1)); echo "SKIP $t" ;;
*)
fail=$((fail + 1)) failed="$failed $t"
echo "FAIL $t (exit $rc)"
# These assert with `test "$(...)" == "..." || exit 1`, which
# says nothing at all on failure. Re-run traced.
bash -x "$t" >>"$t.log" 2>&1 || true
tail -n 25 "$t.log" | sed 's/^/ /'
;;
esac
done
echo "ran=$((pass + fail + skip)) pass=$pass fail=$fail skip=$skip" |
tee -a "$GITHUB_STEP_SUMMARY"
# Every gate in these scripts exits 77, so a suite that degraded to
# all-skipped would report green having tested nothing: assert a floor
# on what actually ran, not just the absence of failures.
[ "$pass" -ge 45 ] || { echo "::error::only $pass tests passed ($skip skipped)"; exit 1; }
[ "$fail" -eq 0 ] || { echo "::error::failing:$failed"; exit 1; }
- name: Upload the test logs
if: always()
uses: actions/upload-artifact@v4
with:
name: engine-tests-${{ matrix.platform }}-${{ matrix.configuration }}
path: tests/*.log
if-no-files-found: warn
- name: Upload MSBuild logs
if: always()

View File

@@ -61,7 +61,7 @@ libhttrack_la_SOURCES = htscore.c htsparse.c htsback.c htscache.c \
htshelp.c htslib.c htscoremain.c \
htsname.c htsrobots.c htstools.c htswizard.c \
htsalias.c htsthread.c htsindex.c htsbauth.c \
htsmd5.c htscodec.c htszlib.c htswrap.c htsconcat.c \
htsmd5.c htscodec.c htsproxy.c htszlib.c htswrap.c htsconcat.c \
htsmodules.c htscharset.c punycode.c htsencoding.c htssniff.c \
md5.c \
minizip/ioapi.c minizip/mztools.c minizip/unzip.c minizip/zip.c \
@@ -72,7 +72,7 @@ libhttrack_la_SOURCES = htscore.c htsparse.c htsback.c htscache.c \
htshelp.h htsindex.h htslib.h htsmd5.h \
htsmodules.h htsname.h htsnet.h htssniff.h \
htsopt.h htsrobots.h htsthread.h \
htstools.h htswizard.h htswrap.h htscodec.h htszlib.h \
htstools.h htswizard.h htswrap.h htscodec.h htsproxy.h htszlib.h \
htsstrings.h htsarrays.h httrack-library.h \
htscharset.h punycode.h htsencoding.h \
htsentities.h htsentities.sh htsbasiccharsets.sh htscodepages.h \

View File

@@ -45,6 +45,7 @@ Please visit our Website: http://www.httrack.com
#include "htsftp.h"
#include "htscodec.h"
#include "htsproxy.h"
#if HTS_USEZLIB
#include "htszlib.h"
#else
@@ -775,7 +776,7 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
if (back[p].r.statuscode == HTTP_OK) { // OK (ou 304 en backing)
if (back[p].r.is_write) { // Written file
if (may_be_hypertext_mime(opt, back[p].r.contenttype, back[p].url_fil)) { // to parse!
off_t sz;
LLint sz;
sz = fsize_utf8(back[p].url_sav);
if (sz > 0) { // ok, exists!
@@ -1952,7 +1953,7 @@ int back_add(struct_back * sback, httrackp * opt, cache_back * cache, const char
/* Not in cache ; maybe in temporary cache ? Warning: non-movable
"url_sav" */
else if (back_unserialize_ref(opt, adr, fil, &itemback) == 0) {
const off_t file_size = fsize_utf8(itemback->url_sav);
const LLint file_size = fsize_utf8(itemback->url_sav);
/* Found file on disk */
if (file_size > 0) {
@@ -1987,7 +1988,7 @@ int back_add(struct_back * sback, httrackp * opt, cache_back * cache, const char
}
/* Not in cache or temporary cache ; found on disk ? (hack) */
else if (fexist_utf8(save)) {
const off_t sz = fsize_utf8(save);
const LLint sz = fsize_utf8(save);
// Bon, là il est possible que le fichier ait été partiellement transféré
// (s'il l'avait été en totalité il aurait été inscrit dans le cache ET existerait sur disque)
@@ -3694,7 +3695,7 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
if (back[i].r.statuscode == HTTP_OK && !back[i].testmode) { // 'OK'
if (!is_hypertext_mime(opt, back[i].r.contenttype, back[i].url_fil)) { // not HTML
if (strnotempty(back[i].url_sav)) { // target found
off_t size = fsize_utf8(back[i].url_sav);
LLint size = fsize_utf8(back[i].url_sav);
if (size >= 0) {
if (back[i].r.totalsize == size) { // same size!
@@ -3913,7 +3914,7 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
// traiter 206 (partial content)
// xxc SI CHUNK VERIFIER QUE CA MARCHE??
if (back[i].r.statuscode == 206) { // on nous envoie un morceau (la fin) coz une partie sur disque!
off_t sz = fsize_utf8(back[i].url_sav);
LLint sz = fsize_utf8(back[i].url_sav);
/* RFC 7233: resume at the server's Content-Range start,
not the offset we requested; a server may resume
earlier and appending the overlap duplicates bytes

View File

@@ -356,7 +356,7 @@ void cache_add(httrackp * opt, cache_back * cache, const htsblk * r,
FILE *fp;
// On recopie le fichier->.
off_t file_size = fsize_utf8(fconv(catbuff, sizeof(catbuff), url_save));
LLint file_size = fsize_utf8(fconv(catbuff, sizeof(catbuff), url_save));
if (file_size >= 0) {
fp = FOPEN(fconv(catbuff, sizeof(catbuff), url_save), "rb");
@@ -1241,23 +1241,27 @@ char *readfile(const char *fil) {
char *readfile2(const char *fil, LLint * size) {
char *adr = NULL;
char catbuff[CATBUFF_SIZE];
INTsys len = 0;
const LLint len = fsize(fil);
len = fsize(fil);
if (len >= 0) { // exists
/* a size too large for size_t (32-bit Windows/i386) must fail closed: it
would wrap malloct() short while fread() still read the untruncated len */
const size_t buflen = len >= 0 ? llint_to_size_t(len) : (size_t) -1;
if (buflen != (size_t) -1) { // exists, and is addressable
FILE *fp;
fp = fopen(fconv(catbuff, sizeof(catbuff), fil), "rb");
if (fp != NULL) { // n'existe pas (!)
adr = (char *) malloct(len + 1);
adr = (char *) malloct(buflen + 1);
if (size != NULL)
*size = len;
if (adr != NULL) {
if (len > 0 && fread(adr, 1, len, fp) != len) { // fichier endommagé ?
if (buflen > 0 &&
fread(adr, 1, buflen, fp) != buflen) { // fichier endommagé ?
freet(adr);
adr = NULL;
} else
*(adr + len) = '\0';
*(adr + buflen) = '\0';
}
fclose(fp);
}
@@ -1269,19 +1273,21 @@ char *readfile2(const char *fil, LLint * size) {
char *readfile_utf8(const char *fil) {
char *adr = NULL;
char catbuff[CATBUFF_SIZE];
const off_t len = fsize_utf8(fil);
const LLint len = fsize_utf8(fil);
const size_t buflen = len >= 0 ? llint_to_size_t(len) : (size_t) -1;
if (len >= 0) { // exists
if (buflen != (size_t) -1) { // exists, and is addressable (see readfile2)
FILE *const fp = FOPEN(fconv(catbuff, sizeof(catbuff), fil), "rb");
if (fp != NULL) { // n'existe pas (!)
adr = (char *) malloct(len + 1);
adr = (char *) malloct(buflen + 1);
if (adr != NULL) {
if (len > 0 && fread(adr, 1, len, fp) != len) { // fichier endommagé ?
if (buflen > 0 &&
fread(adr, 1, buflen, fp) != buflen) { // fichier endommagé ?
freet(adr);
adr = NULL;
} else {
adr[len] = '\0';
adr[buflen] = '\0';
}
}
fclose(fp);

View File

@@ -938,9 +938,9 @@ static void reconcile_put(httrackp *opt, const char *name, size_t size) {
}
/* Expect `name` to weigh `size` bytes, or be absent when size == -1. */
static int reconcile_expect(httrackp *opt, const char *name, off_t size,
static int reconcile_expect(httrackp *opt, const char *name, LLint size,
const char *what) {
const off_t got = fsize(reconcile_st_path(opt, name));
const LLint got = fsize(reconcile_st_path(opt, name));
if (got != size) {
fprintf(stderr, "cache-reconcile: %s: %s is %d bytes, expected %d\n", what,
@@ -954,7 +954,7 @@ int cache_reconcile_selftest(httrackp *opt, const char *dir) {
int failures = 0;
/* around the interrupted-run thresholds (new < 32768, old > 65536) */
static const off_t TINY = 1024, MID = 40000, SOLID = 131072;
static const LLint TINY = 1024, MID = 40000, SOLID = 131072;
golden_setup(opt, dir);
#ifdef _WIN32

View File

@@ -31,6 +31,9 @@ Please visit our Website: http://www.httrack.com
/* ------------------------------------------------------------ */
#include "htscharset.h"
#ifdef _WIN32
#include <shellapi.h>
#endif
#include "htsbase.h"
#include "punycode.h"
#include "htssafe.h"
@@ -386,6 +389,40 @@ char *hts_convertStringSystemToUTF8(const char *s, size_t size) {
return hts_convertStringCPToUTF8(s, size, GetACP());
}
HTSEXT_API void hts_argv_utf8(int *pargc, char ***pargv) {
int wargc = 0;
LPWSTR *const wargv = CommandLineToArgvW(GetCommandLineW(), &wargc);
char **argv;
int i;
// On any failure keep the CRT's ANSI argv: lossy, but never half-converted.
if (wargv == NULL)
return;
if (wargc <= 0 ||
(argv = calloct((size_t) wargc + 1, sizeof(char *))) == NULL) {
LocalFree(wargv);
return;
}
for (i = 0; i < wargc; i++) {
const int wsize = (int) wcslen(wargv[i]);
// hts_convertUCS2StringToUTF8() returns NULL on an empty string
argv[i] =
wsize != 0 ? hts_convertUCS2StringToUTF8(wargv[i], wsize) : strdupt("");
if (argv[i] == NULL) {
while (i-- != 0)
freet(argv[i]);
freet(argv);
LocalFree(wargv);
return;
}
}
argv[wargc] = NULL; // callers may rely on argv[argc] == NULL
LocalFree(wargv);
*pargc = wargc;
*pargv = argv; // never freed: argv lives for the process
}
#else
#include <errno.h>

View File

@@ -34,6 +34,7 @@ Please visit our Website: http://www.httrack.com
#define HTS_CHARSET_DEFH
/** Standard includes. **/
#include "htsglobal.h"
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
@@ -175,6 +176,15 @@ extern char *hts_convertUCS2StringToUTF8(LPWSTR woutput, int wsize);
* This function is WIN32 specific.
**/
extern char *hts_convertStringSystemToUTF8(const char *s, size_t size);
/**
* Replace the CRT's ANSI argv by a UTF-8 one decoded from the real UTF-16
* command line: every char* is UTF-8 on Windows (FOPEN, STAT, ... convert at
* the syscall boundary). Keeps the CRT's argv on failure; the new array is
* writable, NULL-terminated, and lives for the process.
* This function is WIN32 specific.
**/
HTSEXT_API void hts_argv_utf8(int *pargc, char ***pargv);
#endif
#endif

View File

@@ -855,7 +855,7 @@ int httpmirror(char *url1, httrackp * opt) {
char *filelist_buff = NULL;
size_t filelist_sz = 0;
const char *filelist_err = NULL; /* failure reason, NULL on success */
const off_t fs = fsize(StringBuff(opt->filelist));
const LLint fs = fsize(StringBuff(opt->filelist));
if (fs < 0) {
/* fsize() hides the cause; redo stat() for a precise errno (#49) */
@@ -863,7 +863,7 @@ int httpmirror(char *url1, httrackp * opt) {
filelist_err = stat(StringBuff(opt->filelist), &st) != 0
? strerror(errno)
: "not a regular file";
} else if ((filelist_sz = off_t_to_size_t(fs)) == (size_t) -1) {
} else if ((filelist_sz = llint_to_size_t(fs)) == (size_t) -1) {
filelist_err = "file too large";
filelist_sz = 0;
} else {
@@ -2089,10 +2089,9 @@ int httpmirror(char *url1, httrackp * opt) {
(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log),
"hts-cache/old.lst"), "rb");
if (old_lst) {
const size_t sz =
off_t_to_size_t(fsize(fconcat
(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log),
"hts-cache/new.lst")));
const size_t sz = llint_to_size_t(
fsize(fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-cache/new.lst")));
new_lst =
fopen(fconcat
(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log),

View File

@@ -1509,7 +1509,7 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
htsmain_free();
return -1;
} else {
off_t fz;
LLint fz;
na++;
fz = fsize(argv[na]);
@@ -2584,21 +2584,11 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
hts_get_version_info(opt), t, url);
fprintf(opt->log, "(");
for(i = 0; i < argc; i++) {
#ifdef _WIN32
char *carg =
hts_convertStringSystemToUTF8(argv[i], (int) strlen(argv[i]));
char *arg = carg != NULL ? carg : argv[i];
#else
const char *arg = argv[i];
#endif
const char *arg = argv[i]; // already UTF-8 on every platform
if (strchr(arg, ' ') == NULL || strchr(arg, '\"') != NULL)
fprintf(opt->log, "%s ", arg);
else // entre "" (si espace(s) et pas déja de ")
fprintf(opt->log, "\"%s\" ", arg);
#ifdef _WIN32
if (carg != NULL)
free(carg);
#endif
}
fprintf(opt->log, ")" LF);
fprintf(opt->log, LF);

View File

@@ -122,8 +122,11 @@ int fa_strjoker_dual(int type, char **filters, int nfil, const char *nom1,
}
/* Real filters/URLs fit HTS_URLMAXSIZE*2; past it a hostile pattern recurses
O(len) deep or runs O(n^2*stars). Cap length (depth) and steps (work). */
O(len) deep or runs O(n^2*stars). Cap length, recursion depth and steps
(work): the length cap alone still allows ~2000 frames, ~900KB of stack,
which overflows the 1MB a Windows thread gets (#574). */
#define STRJOKER_MAXLEN (HTS_URLMAXSIZE * 2)
#define STRJOKER_MAXDEPTH 256u
#define STRJOKER_MAXSTEPS 2000000u
/* Failure memo for the recursive matcher: one bit per (chaine, joker) offset
@@ -133,20 +136,28 @@ typedef struct strjoker_memo {
size_t stride; /* strlen(joker0) + 1 */
unsigned char *failed; /* failed-pair bitmap; NULL: no memo */
size_t *nsteps; /* shared work counter; NULL: unbounded (oracle) */
size_t maxdepth; /* deepest recursion reached */
hts_boolean cut; /* a branch hit the depth cap: its failures are not final */
} strjoker_memo;
static const char *strjoker_impl(const strjoker_memo *memo, const char *chaine,
const char *joker, LLint *size,
int *size_flag);
static const char *strjoker_impl(strjoker_memo *memo, const char *chaine,
const char *joker, LLint *size, int *size_flag,
size_t depth);
/* A pair that failed once fails forever (*size is only written on the success
path), so record NULL results and cut the re-exploration. */
static const char *strjoker_rec(const strjoker_memo *memo, const char *chaine,
const char *joker, LLint *size,
int *size_flag) {
static const char *strjoker_rec(strjoker_memo *memo, const char *chaine,
const char *joker, LLint *size, int *size_flag,
size_t depth) {
size_t bit = 0;
const char *adr;
if (depth > memo->maxdepth)
memo->maxdepth = depth;
if (depth >= STRJOKER_MAXDEPTH) {
memo->cut = HTS_TRUE;
return NULL; /* nesting beyond any real filter: fail the branch safely */
}
if (memo->nsteps != NULL && ++*memo->nsteps > STRJOKER_MAXSTEPS)
return NULL; /* work budget spent: fail the match safely */
if (memo->failed) {
@@ -155,8 +166,9 @@ static const char *strjoker_rec(const strjoker_memo *memo, const char *chaine,
if (memo->failed[bit >> 3] & (unsigned char) (1u << (bit & 7)))
return NULL;
}
adr = strjoker_impl(memo, chaine, joker, size, size_flag);
if (adr == NULL && memo->failed)
adr = strjoker_impl(memo, chaine, joker, size, size_flag, depth + 1);
/* a cut branch may fail here yet match when reached at a shallower depth */
if (adr == NULL && memo->failed && !memo->cut)
memo->failed[bit >> 3] |= (unsigned char) (1u << (bit & 7));
return adr;
}
@@ -164,13 +176,15 @@ static const char *strjoker_rec(const strjoker_memo *memo, const char *chaine,
/* Match chaine against joker with a shared work budget *nsteps (a strjokerfind
sweep passes one counter, bounding the whole scan). */
static const char *strjoker_bounded(const char *chaine, const char *joker,
LLint *size, int *size_flag,
size_t *nsteps) {
strjoker_memo memo = {chaine, joker, 0, NULL, nsteps};
LLint *size, int *size_flag, size_t *nsteps,
size_t *maxdepth) {
strjoker_memo memo = {chaine, joker, 0, NULL, nsteps, 0, HTS_FALSE};
unsigned char stackbits[2048];
hts_boolean onheap = HTS_FALSE;
const char *adr;
if (maxdepth != NULL)
*maxdepth = 0;
if (chaine != NULL && joker != NULL) {
const size_t l1 = strlen(chaine), l2 = strlen(joker);
@@ -189,9 +203,11 @@ static const char *strjoker_bounded(const char *chaine, const char *joker,
}
}
}
adr = strjoker_rec(&memo, chaine, joker, size, size_flag);
adr = strjoker_rec(&memo, chaine, joker, size, size_flag, 0);
if (onheap)
freet(memo.failed);
if (maxdepth != NULL)
*maxdepth = memo.maxdepth;
return adr;
}
@@ -202,34 +218,39 @@ HTS_INLINE const char *strjoker(const char *chaine, const char *joker,
LLint *size, int *size_flag) {
size_t nsteps = 0;
return strjoker_bounded(chaine, joker, size, size_flag, &nsteps);
return strjoker_bounded(chaine, joker, size, size_flag, &nsteps, NULL);
}
/* Test-only oracle: the same matcher without the failure memo. */
const char *strjoker_nomemo(const char *chaine, const char *joker, LLint *size,
int *size_flag) {
const strjoker_memo memo = {chaine, joker, 0, NULL};
strjoker_memo memo = {chaine, joker, 0, NULL, NULL, 0, HTS_FALSE};
return strjoker_rec(&memo, chaine, joker, size, size_flag);
return strjoker_rec(&memo, chaine, joker, size, size_flag, 0);
}
/* Test-only: strjoker() reporting the work-budget steps spent and the cap, so a
self-test can prove the budget bounds a hostile pattern's work. */
const char *strjoker_steps(const char *chaine, const char *joker,
size_t *nsteps_out, size_t *maxsteps_out) {
size_t nsteps = 0;
const char *r = strjoker_bounded(chaine, joker, NULL, NULL, &nsteps);
/* Test-only: strjoker() reporting the work and depth spent and their caps, so a
self-test can prove both bound a hostile pattern. */
const char *strjoker_bounds(const char *chaine, const char *joker,
size_t *nsteps_out, size_t *maxsteps_out,
size_t *depth_out, size_t *maxdepth_out) {
size_t nsteps = 0, depth = 0;
const char *r = strjoker_bounded(chaine, joker, NULL, NULL, &nsteps, &depth);
if (nsteps_out != NULL)
*nsteps_out = nsteps;
if (maxsteps_out != NULL)
*maxsteps_out = STRJOKER_MAXSTEPS;
if (depth_out != NULL)
*depth_out = depth;
if (maxdepth_out != NULL)
*maxdepth_out = STRJOKER_MAXDEPTH;
return r;
}
static const char *strjoker_impl(const strjoker_memo *memo, const char *chaine,
const char *joker, LLint *size,
int *size_flag) {
static const char *strjoker_impl(strjoker_memo *memo, const char *chaine,
const char *joker, LLint *size, int *size_flag,
size_t depth) {
if (strnotempty(joker) == 0) { // fin de chaine joker
if (strnotempty(chaine) == 0) // fin aussi pour la chaine: ok
return chaine;
@@ -403,7 +424,8 @@ static const char *strjoker_impl(const strjoker_memo *memo, const char *chaine,
// tester sans le joker (pas ()+ mais ()*)
if (!unique) {
if ((adr = strjoker_rec(memo, chaine, joker + jmp, size, size_flag))) {
if ((adr = strjoker_rec(memo, chaine, joker + jmp, size, size_flag,
depth))) {
return adr;
}
}
@@ -416,7 +438,7 @@ static const char *strjoker_impl(const strjoker_memo *memo, const char *chaine,
while(i < (int) max) {
if (pass[(int) (unsigned char) chaine[i]]) { // caractère autorisé
if ((adr = strjoker_rec(memo, chaine + i + 1, joker + jmp, size,
size_flag))) {
size_flag, depth))) {
return adr;
}
i++;
@@ -427,7 +449,7 @@ static const char *strjoker_impl(const strjoker_memo *memo, const char *chaine,
// tester chaîne vide
if (i != max + 2) // avant c'est ok
if ((adr = strjoker_rec(memo, chaine + max, joker + jmp, size,
size_flag)))
size_flag, depth)))
return adr;
return NULL; // perdu
@@ -449,7 +471,8 @@ static const char *strjoker_impl(const strjoker_memo *memo, const char *chaine,
// comparaison ok?
if (ok) {
// continuer la comparaison.
if (strjoker_rec(memo, chaine + jmp, joker + jmp, size, size_flag))
if (strjoker_rec(memo, chaine + jmp, joker + jmp, size, size_flag,
depth))
return chaine; // retourner 1e lettre
}
@@ -471,8 +494,8 @@ const char *strjokerfind(const char *chaine, const char *joker) {
if (chaine != NULL && strlen(chaine) > STRJOKER_MAXLEN)
return NULL;
while(*chaine) {
if ((adr = strjoker_bounded(chaine, joker, NULL, NULL,
&nsteps))) { // ok trouvé
if ((adr = strjoker_bounded(chaine, joker, NULL, NULL, &nsteps,
NULL))) { // ok trouvé
return adr;
}
if (nsteps > STRJOKER_MAXSTEPS) // scan budget spent: no match

View File

@@ -52,10 +52,12 @@ HTS_INLINE const char *strjoker(const char *chaine, const char *joker, LLint * s
oracle for the memoized matcher. */
const char *strjoker_nomemo(const char *chaine, const char *joker, LLint *size,
int *size_flag);
/* strjoker() reporting the work-budget steps it spent and the cap; test-only,
lets a self-test assert the budget bounds a hostile pattern's work. */
const char *strjoker_steps(const char *chaine, const char *joker,
size_t *nsteps_out, size_t *maxsteps_out);
/* strjoker() reporting the work-budget steps and the recursion depth it spent,
with their caps; test-only, lets a self-test assert both bound a hostile
pattern (depth bounds the stack: an uncapped one overflows Windows' 1MB). */
const char *strjoker_bounds(const char *chaine, const char *joker,
size_t *nsteps_out, size_t *maxsteps_out,
size_t *depth_out, size_t *maxdepth_out);
const char *strjokerfind(const char *chaine, const char *joker);
#endif

View File

@@ -322,16 +322,15 @@ typedef int64_t TStamp;
/* Full printf conversion, '%' included (PRId64 has none): "X: " LLintP. */
#define LLintP "%" PRId64
/* Integer type for file offsets/sizes passed to the C library. Widens to
LLint (with HTS_FSEEKO for fseeko/ftello) under large-file support, plain
int otherwise; INTsysP is its printf conversion. */
#ifdef LFS_FLAG
/* Integer type for file offsets/sizes passed to the C library; INTsysP is its
printf conversion. FIXME: LFS_FLAG is a configure make variable, never a C
macro, so this test is dead and INTsys stays int on POSIX despite large-file
support (the real macro is HTS_LFS). Widening it there is an installed-header
type change, so it is left alone here. */
#if defined(LFS_FLAG) || defined(_MSC_VER)
typedef LLint INTsys;
#define INTsysP LLintP
#ifdef __linux
#define HTS_FSEEKO
#endif
#else
typedef int INTsys;

View File

@@ -318,7 +318,7 @@ void index_finish(const char *indexpath, int mode) {
char catbuff[CATBUFF_SIZE];
char **tab;
char *blk;
off_t size = fpsize(fp_tmpproject);
LLint size = fpsize(fp_tmpproject);
if (size > 0) {
if (fp_tmpproject) {

View File

@@ -649,504 +649,6 @@ T_SOC http_fopen(httrackp * opt, const char *adr, const char *fil, htsblk * reto
return http_xfopen(opt, 0, 1, 1, NULL, adr, fil, retour);
}
// Read a CRLF line from a non-blocking socket (waits up to timeout per recv).
// Returns the line length (0 = empty), or -1 on timeout/EOF/error.
static int proxy_getline(T_SOC soc, char *s, int max, int timeout) {
int j = 0;
for (;;) {
unsigned char ch;
int n;
if (!check_readinput_t(soc, timeout))
return -1; // timed out waiting for data
n = (int) recv(soc, &ch, 1, 0);
if (n == 1) {
if (ch == 13) // CR
continue;
if (ch == 10) // LF: end of line
break;
if (j >= max - 1)
return -1; // line too long: bound the read against a hostile proxy
s[j++] = (char) ch;
} else if (n == 0) {
return -1; // connection closed
} else {
#ifdef _WIN32
if (WSAGetLastError() == WSAEWOULDBLOCK)
continue;
#else
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)
continue;
#endif
return -1;
}
}
s[j] = '\0';
return j;
}
int http_proxy_tunnel(httrackp *opt, htsblk *retour, const char *adr,
int timeout) {
const T_SOC soc = retour->soc;
const char *const host = jump_identification_const(adr); // host[:port]
const char *const portsep = jump_toport_const(adr); // ":port" or NULL
char BIGSTK authority[HTS_URLMAXSIZE * 2];
char BIGSTK req[HTS_URLMAXSIZE * 4 + 1100];
char line[1024];
int code;
if (soc == INVALID_SOCKET)
return 0;
// CONNECT needs an explicit host:port; default the https port
authority[0] = '\0';
if (portsep != NULL)
strlcatbuff(authority, host, sizeof(authority)); // already host:port
else
snprintf(authority, sizeof(authority), "%s:%d", host, 443);
// backstop: never let a stray CR/LF in the host smuggle a second line into
// the CONNECT request (the host is already sanitized upstream)
{
const char *c;
for (c = authority; *c != '\0'; c++) {
if ((unsigned char) *c < ' ') {
strcpybuff(retour->msg, "proxy CONNECT: invalid host");
return 0;
}
}
}
snprintf(req, sizeof(req), "CONNECT %s HTTP/1.0" H_CRLF "Host: %s" H_CRLF,
authority, authority);
// creds go on the CONNECT, not the tunneled origin request
if (link_has_authorization(retour->req.proxy.name)) {
const char *a = jump_identification_const(retour->req.proxy.name);
const char *astart = jump_protocol_const(retour->req.proxy.name);
char autorisation[1100];
char user_pass[256];
autorisation[0] = user_pass[0] = '\0';
strncatbuff(user_pass, astart, (int) (a - astart) - 1);
strcpybuff(user_pass, unescape_http(OPT_GET_BUFF(opt),
OPT_GET_BUFF_SIZE(opt), user_pass));
code64((unsigned char *) user_pass, (int) strlen(user_pass),
(unsigned char *) autorisation, 0);
strlcatbuff(req, "Proxy-Authorization: Basic ", sizeof(req));
strlcatbuff(req, autorisation, sizeof(req));
strlcatbuff(req, H_CRLF, sizeof(req));
}
strlcatbuff(req, H_CRLF, sizeof(req)); // end of request headers
// raw send: ssl is set, so sendc() would route to TLS
{
const char *p = req;
size_t remain = strlen(req);
int stalls = 0;
while (remain > 0) {
const int n = (int) send(soc, p, (int) remain, 0);
if (n > 0) {
p += n;
remain -= (size_t) n;
stalls = 0;
} else {
#ifdef _WIN32
const int wouldblock = (WSAGetLastError() == WSAEWOULDBLOCK);
#else
const int wouldblock =
(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR);
#endif
// don't spin forever on a fatal error or an unwritable socket
if (!wouldblock || !check_writeinput_t(soc, timeout) ||
++stalls > 100) {
strcpybuff(retour->msg, "proxy CONNECT: write error");
return 0;
}
}
}
}
// proxy status line: "HTTP/1.x <code> ..."
if (proxy_getline(soc, line, sizeof(line), timeout) < 0) {
strcpybuff(retour->msg, "proxy CONNECT: no response");
return 0;
}
if (sscanf(line, "HTTP/%*d.%*d %d", &code) < 1)
code = 0;
if (code < 200 || code >= 300) {
snprintf(retour->msg, sizeof(retour->msg), "proxy CONNECT refused: %s",
strnotempty(line) ? line : "(no status)");
return 0;
}
// drain headers to the blank line; cap the count so a flooding proxy can't
// stall the crawl
{
int headers = 0;
for (;;) {
const int n = proxy_getline(soc, line, sizeof(line), timeout);
if (n < 0) {
strcpybuff(retour->msg, "proxy CONNECT: truncated response");
return 0;
}
if (n == 0)
break; // blank line: tunnel ready
if (++headers > 64) {
strcpybuff(retour->msg, "proxy CONNECT: too many response headers");
return 0;
}
}
}
return 1;
}
/* SOCKS5 client (RFC 1928, RFC 1929 auth), hostname mode only: the proxy
resolves the origin name (remote DNS, curl's socks5h). The stream is the
proxy socket, or a scripted buffer under -#test=socks5. */
#define SOCKS5_VERSION 0x05
#define SOCKS5_AUTH_VERSION 0x01
#define SOCKS5_METHOD_NONE 0x00
#define SOCKS5_METHOD_USERPASS 0x02
#define SOCKS5_METHOD_NOACCEPTABLE 0xFF
#define SOCKS5_CMD_CONNECT 0x01
#define SOCKS5_ATYP_IPV4 0x01
#define SOCKS5_ATYP_DOMAIN 0x03
#define SOCKS5_ATYP_IPV6 0x04
#define SOCKS5_MAXFIELD 255 /* one length byte: host, user and password */
typedef struct socks5_stream {
T_SOC soc; /* INVALID_SOCKET when scripted (self-test) */
int timeout;
socks5_test_io *io;
} socks5_stream;
static int socks5_fail(char *msg, size_t msgsize, const char *text) {
if (msgsize != 0)
strlcpybuff(msg, text, msgsize);
return 0;
}
/* Read exactly n bytes, or fail: SOCKS frames are not self-delimiting, so a
short read would desync the stream shared with the origin traffic. */
static int socks5_read_n(socks5_stream *st, unsigned char *buf, size_t n) {
size_t got = 0;
int stalls = 0;
if (st->soc == INVALID_SOCKET) { /* scripted */
socks5_test_io *const io = st->io;
if (n > io->reply_len - io->consumed)
return 0;
memcpy(buf, io->reply + io->consumed, n);
io->consumed += n;
return 1;
}
while (got < n) {
const int r = (int) recv(st->soc, (char *) buf + got, (int) (n - got), 0);
if (r > 0) {
got += (size_t) r;
stalls = 0;
} else if (r == 0) {
return 0; // proxy closed mid-frame
} else {
#ifdef _WIN32
const int wouldblock = (WSAGetLastError() == WSAEWOULDBLOCK);
#else
const int wouldblock =
(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR);
#endif
if (!wouldblock || !check_readinput_t(st->soc, st->timeout) ||
++stalls > 100)
return 0;
}
}
return 1;
}
static int socks5_write_all(socks5_stream *st, const unsigned char *buf,
size_t len) {
size_t remain = len;
int stalls = 0;
if (st->soc == INVALID_SOCKET) { /* scripted */
socks5_test_io *const io = st->io;
if (len > sizeof(io->sent) - io->sent_len)
return 0;
memcpy(io->sent + io->sent_len, buf, len);
io->sent_len += len;
return 1;
}
while (remain > 0) {
// raw send: the socket is still plain here, sendc() would route to TLS
const int n = (int) send(st->soc, (const char *) buf, (int) remain, 0);
if (n > 0) {
buf += n;
remain -= (size_t) n;
stalls = 0;
} else {
#ifdef _WIN32
const int wouldblock = (WSAGetLastError() == WSAEWOULDBLOCK);
#else
const int wouldblock =
(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR);
#endif
if (!wouldblock || !check_writeinput_t(st->soc, st->timeout) ||
++stalls > 100)
return 0;
}
}
return 1;
}
static const char *socks5_rep_message(unsigned char rep) {
switch (rep) {
case 0x01:
return "general failure";
case 0x02:
return "connection not allowed by ruleset";
case 0x03:
return "network unreachable";
case 0x04:
return "host unreachable";
case 0x05:
return "connection refused";
case 0x06:
return "TTL expired";
case 0x07:
return "command not supported";
case 0x08:
return "address type not supported";
default:
return "unknown error";
}
}
/* The reply's BND.ADDR is variable-length: consume exactly what ATYP says, or
the leftover bytes prepend to the first origin (or TLS) read. */
static int socks5_read_reply(socks5_stream *st, char *msg, size_t msgsize) {
unsigned char head[4];
unsigned char addr[SOCKS5_MAXFIELD + 2];
size_t addrlen;
if (!socks5_read_n(st, head, sizeof(head)))
return socks5_fail(msg, msgsize, "SOCKS5: no reply from proxy");
if (head[0] != SOCKS5_VERSION)
return socks5_fail(msg, msgsize, "SOCKS5: bad version in reply");
if (head[1] != 0x00) {
snprintf(msg, msgsize, "SOCKS5 connect failed: %s",
socks5_rep_message(head[1]));
return 0;
}
switch (head[3]) {
case SOCKS5_ATYP_IPV4:
addrlen = 4;
break;
case SOCKS5_ATYP_IPV6:
addrlen = 16;
break;
case SOCKS5_ATYP_DOMAIN: {
unsigned char len;
if (!socks5_read_n(st, &len, 1))
return socks5_fail(msg, msgsize, "SOCKS5: truncated reply");
addrlen = len; // <= 255, always fits addr[]
break;
}
default: // unknown length: the stream cannot be resynchronized
return socks5_fail(msg, msgsize, "SOCKS5: unknown address type in reply");
}
if (addrlen != 0 && !socks5_read_n(st, addr, addrlen))
return socks5_fail(msg, msgsize, "SOCKS5: truncated reply address");
if (!socks5_read_n(st, addr, 2)) // BND.PORT, unused
return socks5_fail(msg, msgsize, "SOCKS5: truncated reply port");
return 1;
}
static int socks5_negotiate(socks5_stream *st, const char *host, size_t hostlen,
int port, const char *user, size_t userlen,
const char *pass, size_t passlen, int want_auth,
char *msg, size_t msgsize) {
unsigned char frame[3 + SOCKS5_MAXFIELD + SOCKS5_MAXFIELD];
unsigned char rep[2];
/* greeting */
frame[0] = SOCKS5_VERSION;
frame[1] = (unsigned char) (want_auth ? 2 : 1);
frame[2] = SOCKS5_METHOD_NONE;
frame[3] = SOCKS5_METHOD_USERPASS;
if (!socks5_write_all(st, frame, (size_t) 2 + frame[1]))
return socks5_fail(msg, msgsize, "SOCKS5: write error");
if (!socks5_read_n(st, rep, sizeof(rep)))
return socks5_fail(msg, msgsize, "SOCKS5: no method reply from proxy");
if (rep[0] != SOCKS5_VERSION)
return socks5_fail(msg, msgsize, "SOCKS5: bad version in method reply");
switch (rep[1]) {
case SOCKS5_METHOD_NONE:
break;
case SOCKS5_METHOD_USERPASS:
if (!want_auth)
return socks5_fail(msg, msgsize,
"SOCKS5: proxy requires authentication, none given");
/* RFC 1929 sub-negotiation; its version byte is 0x01, not 0x05 */
frame[0] = SOCKS5_AUTH_VERSION;
frame[1] = (unsigned char) userlen;
memcpy(frame + 2, user, userlen);
frame[2 + userlen] = (unsigned char) passlen;
memcpy(frame + 3 + userlen, pass, passlen);
if (!socks5_write_all(st, frame, 3 + userlen + passlen))
return socks5_fail(msg, msgsize, "SOCKS5: write error");
if (!socks5_read_n(st, rep, sizeof(rep)))
return socks5_fail(msg, msgsize, "SOCKS5: no authentication reply");
if (rep[1] != 0x00)
return socks5_fail(msg, msgsize, "SOCKS5: authentication failed");
break;
case SOCKS5_METHOD_NOACCEPTABLE:
return socks5_fail(
msg, msgsize,
"SOCKS5: proxy accepts no authentication method we offer");
default:
return socks5_fail(msg, msgsize,
"SOCKS5: proxy selected an unknown method");
}
/* CONNECT to the origin, by name */
frame[0] = SOCKS5_VERSION;
frame[1] = SOCKS5_CMD_CONNECT;
frame[2] = 0x00; // RSV
frame[3] = SOCKS5_ATYP_DOMAIN;
frame[4] = (unsigned char) hostlen;
memcpy(frame + 5, host, hostlen);
frame[5 + hostlen] = (unsigned char) (port >> 8);
frame[6 + hostlen] = (unsigned char) (port & 0xFF);
if (!socks5_write_all(st, frame, 7 + hostlen))
return socks5_fail(msg, msgsize, "SOCKS5: write error");
return socks5_read_reply(st, msg, msgsize);
}
/* Decode the proxy userinfo into the two RFC 1929 fields. Split on the first
colon of the still-escaped string, so a %3A stays inside the username. */
static int socks5_credentials(httrackp *opt, const char *proxy_name, char *user,
size_t user_size, size_t *userlen, char *pass,
size_t pass_size, size_t *passlen, char *msg,
size_t msgsize) {
const char *const a = jump_identification_const(proxy_name); // past the '@'
const char *const astart = jump_protocol_const(proxy_name);
char userinfo[1024];
char *colon;
if (a <= astart || (size_t) (a - astart) - 1 >= sizeof(userinfo))
return socks5_fail(msg, msgsize, "SOCKS5: credentials too long");
userinfo[0] = '\0';
strncatbuff(userinfo, astart, (int) (a - astart) - 1);
colon = strchr(userinfo, ':');
if (colon != NULL)
*colon++ = '\0';
strlcpybuff(
user, unescape_http(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), userinfo),
user_size);
strlcpybuff(pass,
colon != NULL ? unescape_http(OPT_GET_BUFF(opt),
OPT_GET_BUFF_SIZE(opt), colon)
: "",
pass_size);
*userlen = strlen(user);
*passlen = strlen(pass);
// reject, never truncate: a clipped secret would authenticate as another one
if (*userlen > SOCKS5_MAXFIELD || *passlen > SOCKS5_MAXFIELD)
return socks5_fail(msg, msgsize, "SOCKS5: credentials too long");
if (*userlen == 0)
return socks5_fail(msg, msgsize, "SOCKS5: empty proxy username");
return 1;
}
static int socks5_handshake_stream(httrackp *opt, socks5_stream *st,
const char *adr, const char *proxy_name,
int ssl, char *msg, size_t msgsize) {
const char *const host = jump_identification_const(adr);
const char *const portsep = jump_toport_const(adr);
const size_t hostlen =
portsep != NULL ? (size_t) (portsep - host) : strlen(host);
// sized for the whole userinfo: decoding shrinks, so an over-long credential
// lands intact and is rejected below rather than silently clipped
char user[1024];
char pass[1024];
size_t userlen = 0, passlen = 0;
int want_auth = 0;
int port = ssl ? 443 : 80;
size_t i;
if (hostlen == 0 || hostlen > SOCKS5_MAXFIELD)
return socks5_fail(msg, msgsize, "SOCKS5: invalid origin host");
if (host[0] == '[') // ATYP=domain cannot carry an IPv6 literal
return socks5_fail(msg, msgsize,
"SOCKS5: IPv6 literal origin is not supported");
for (i = 0; i < hostlen; i++) {
if ((unsigned char) host[i] < ' ')
return socks5_fail(msg, msgsize, "SOCKS5: invalid origin host");
}
if (portsep != NULL) {
int p = -1;
sscanf(portsep + 1, "%d", &p);
if (p <= 0 || p > 65535)
return socks5_fail(msg, msgsize, "SOCKS5: invalid origin port");
port = p;
}
if (link_has_authorization(proxy_name)) {
if (!socks5_credentials(opt, proxy_name, user, sizeof(user), &userlen, pass,
sizeof(pass), &passlen, msg, msgsize))
return 0;
want_auth = 1;
}
return socks5_negotiate(st, host, hostlen, port, user, userlen, pass, passlen,
want_auth, msg, msgsize);
}
int socks5_handshake(httrackp *opt, htsblk *retour, const char *adr,
int timeout) {
socks5_stream st;
#if HTS_USEOPENSSL
const int ssl = retour->ssl;
#else
const int ssl = 0;
#endif
if (retour->soc == INVALID_SOCKET)
return 0;
st.soc = retour->soc;
st.timeout = timeout;
st.io = NULL;
return socks5_handshake_stream(opt, &st, adr, retour->req.proxy.name, ssl,
retour->msg, sizeof(retour->msg));
}
int socks5_handshake_scripted(httrackp *opt, const char *adr,
const char *proxy_name, socks5_test_io *io) {
socks5_stream st;
st.soc = INVALID_SOCKET;
st.timeout = 0;
st.io = io;
io->consumed = 0;
io->sent_len = 0;
io->msg[0] = '\0';
return socks5_handshake_stream(opt, &st, adr, proxy_name, 0, io->msg,
sizeof(io->msg));
}
// ouverture d'une liaison http, envoi d'une requète
// mode: 0 GET 1 HEAD [2 POST]
// treat: traiter header?
@@ -5089,26 +4591,31 @@ int fexist_utf8(const char *s) {
return 0;
}
/* Taille d'un fichier, -1 si n'existe pas */
/* File size, -1 if absent */
/* Note: NOT utf-8 */
off_t fsize(const char *s) {
struct stat st;
LLint fsize(const char *s) {
STRUCT_STAT st;
if (!strnotempty(s)) // nom vide: erreur
if (!strnotempty(s)) // empty name: error
return -1;
/* _stat64, not stat(): the latter is 32-bit on MSVC */
#ifdef _WIN32
if (_stat64(s, &st) == 0 && S_ISREG(st.st_mode)) {
#else
if (stat(s, &st) == 0 && S_ISREG(st.st_mode)) {
#endif
return st.st_size;
} else {
return -1;
}
}
/* Taille d'un fichier, -1 si n'existe pas */
/* File size, -1 if absent */
/* Note: utf-8 */
off_t fsize_utf8(const char *s) {
LLint fsize_utf8(const char *s) {
STRUCT_STAT st;
if (!strnotempty(s)) // nom vide: erreur
if (!strnotempty(s)) // empty name: error
return -1;
if (STAT(s, &st) == 0 && S_ISREG(st.st_mode)) {
return st.st_size;
@@ -5117,24 +4624,17 @@ off_t fsize_utf8(const char *s) {
}
}
off_t fpsize(FILE * fp) {
off_t oldpos, size;
/* fseeko/ftello, never fseek/ftell: ftell returns long, which cannot carry an
offset past 2GB on a 32-bit platform and fails with EOVERFLOW there. */
LLint fpsize(FILE *fp) {
LLint oldpos, size;
if (!fp)
return -1;
#ifdef HTS_FSEEKO
oldpos = ftello(fp);
#else
oldpos = ftell(fp);
#endif
fseek(fp, 0, SEEK_END);
#ifdef HTS_FSEEKO
fseeko(fp, 0, SEEK_END);
size = ftello(fp);
fseeko(fp, oldpos, SEEK_SET);
#else
size = ftell(fp);
fseek(fp, oldpos, SEEK_SET);
#endif
return size;
}
@@ -6898,13 +6398,13 @@ int hts_stat_utf8(const char *path, STRUCT_STAT * buf) {
LPWSTR wpath = hts_convertUTF8StringToUCS2(path, (int) strlen(path), NULL);
if (wpath != NULL) {
const int result = _wstat(wpath, buf);
const int result = _wstat64(wpath, buf);
free(wpath);
return result;
} else {
// Fallback on conversion error.
return _stat(path, buf);
return _stat64(path, buf);
}
}

View File

@@ -207,43 +207,9 @@ int check_readinput(htsblk * r);
int check_readinput_t(T_SOC soc, int timeout);
int check_writeinput_t(T_SOC soc, int timeout);
/* Open an HTTP CONNECT tunnel through the active proxy for an https request:
`retour->soc` must already be TCP-connected to the proxy, and `adr` is the
origin authority (url_adr, e.g. "https://host:port"). Sends the CONNECT
request (with Proxy-Authorization when the proxy carries credentials) and
reads the proxy's status line, so the caller's TLS handshake then runs
end-to-end with the origin. Blocks up to `timeout` seconds. Returns 1 on a
2xx tunnel, 0 on failure (retour->msg/statuscode set). */
int http_proxy_tunnel(httrackp *opt, htsblk *retour, const char *adr,
int timeout);
/* TRUE if this -P proxy name (which keeps its scheme) is a SOCKS5 proxy. */
hts_boolean hts_proxy_is_socks(const char *name);
/* SOCKS5 (RFC 1928, RFC 1929 auth) handshake on the raw proxy socket, before
any TLS: `retour->soc` must be TCP-connected to the proxy and `adr` is the
origin (url_adr). The origin name is sent verbatim (ATYP=domain), so the
proxy resolves it; a bracketed IPv6 literal origin is rejected. Blocks up to
`timeout` seconds. Returns 1 with the stream positioned on the first origin
byte, 0 on failure (retour->msg set). */
int socks5_handshake(httrackp *opt, htsblk *retour, const char *adr,
int timeout);
/* Self-test hook (-#test=socks5): run the handshake against `reply`, a scripted
server byte stream, instead of a socket. `sent` captures the client frames,
`consumed` the reply bytes eaten (the frame-desync guard). */
typedef struct socks5_test_io {
const unsigned char *reply;
size_t reply_len;
size_t consumed;
unsigned char sent[1024];
size_t sent_len;
char msg[256];
} socks5_test_io;
int socks5_handshake_scripted(httrackp *opt, const char *adr,
const char *proxy_name, socks5_test_io *io);
void treathead(t_cookie * cookie, const char *adr, const char *fil, htsblk * retour,
char *rcvd);
void treatfirstline(htsblk * retour, const char *rcvd);
@@ -344,10 +310,11 @@ void cut_path(char *fullpath, char *path, size_t path_size, char *pname,
int fexist(const char *s);
int fexist_utf8(const char *s);
/*LLint fsize(const char* s); */
off_t fpsize(FILE * fp);
off_t fsize(const char *s);
off_t fsize_utf8(const char *s);
/* File size in bytes, -1 if absent or not a regular file. LLint, not off_t:
the latter is a 32-bit long on MSVC, truncating any file of 2GB or more. */
LLint fpsize(FILE *fp);
LLint fsize(const char *s);
LLint fsize_utf8(const char *s);
// Threads
typedef void *(*beginthread_type) (void *);
@@ -427,7 +394,8 @@ void *hts_get_callback(t_hts_htmlcheck_callbacks * callbacks,
HTSEXT_API FILE *hts_fopen_utf8(const char *path, const char *mode);
#define STAT hts_stat_utf8
typedef struct _stat STRUCT_STAT;
/* _stat64: _stat's st_size is a 32-bit long, even in x64 builds */
typedef struct _stat64 STRUCT_STAT;
HTSEXT_API int hts_stat_utf8(const char *path, STRUCT_STAT * buf);
#define UNLINK hts_unlink_utf8
@@ -625,9 +593,9 @@ HTS_STATIC int compare_mime(httrackp * opt, const char *mime, const char *file,
#endif
// returns (size_t) -1 upon error
static HTS_UNUSED size_t off_t_to_size_t(off_t o) {
static HTS_UNUSED size_t llint_to_size_t(LLint o) {
const size_t so = (size_t) o;
if ((off_t) so == o) {
if ((LLint) so == o) {
return so;
} else {
return (size_t) -1;

539
src/htsproxy.c Normal file
View File

@@ -0,0 +1,539 @@
/* ------------------------------------------------------------ */
/*
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 2026 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/>.
Ethical use: we kindly ask that you NOT use this software to harvest email
addresses or to collect any other private information about people. Doing so
would dishonor our work and waste the many hours we have spent on it.
Please visit our Website: http://www.httrack.com
*/
/* ------------------------------------------------------------ */
/* File: Proxy tunneling (HTTP CONNECT, SOCKS5) */
/* Author: Xavier Roche */
/* ------------------------------------------------------------ */
/* Internal engine bytecode */
#define HTS_INTERNAL_BYTECODE
#include "htscore.h"
#include "htslib.h"
#include "htsproxy.h"
#include <string.h>
// Read a CRLF line from a non-blocking socket (waits up to timeout per recv).
// Returns the line length (0 = empty), or -1 on timeout/EOF/error.
static int proxy_getline(T_SOC soc, char *s, int max, int timeout) {
int j = 0;
for (;;) {
unsigned char ch;
int n;
if (!check_readinput_t(soc, timeout))
return -1; // timed out waiting for data
n = (int) recv(soc, &ch, 1, 0);
if (n == 1) {
if (ch == 13) // CR
continue;
if (ch == 10) // LF: end of line
break;
if (j >= max - 1)
return -1; // line too long: bound the read against a hostile proxy
s[j++] = (char) ch;
} else if (n == 0) {
return -1; // connection closed
} else {
#ifdef _WIN32
if (WSAGetLastError() == WSAEWOULDBLOCK)
continue;
#else
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)
continue;
#endif
return -1;
}
}
s[j] = '\0';
return j;
}
int http_proxy_tunnel(httrackp *opt, htsblk *retour, const char *adr,
int timeout) {
const T_SOC soc = retour->soc;
const char *const host = jump_identification_const(adr); // host[:port]
const char *const portsep = jump_toport_const(adr); // ":port" or NULL
char BIGSTK authority[HTS_URLMAXSIZE * 2];
char BIGSTK req[HTS_URLMAXSIZE * 4 + 1100];
char line[1024];
int code;
if (soc == INVALID_SOCKET)
return 0;
// CONNECT needs an explicit host:port; default the https port
authority[0] = '\0';
if (portsep != NULL)
strlcatbuff(authority, host, sizeof(authority)); // already host:port
else
snprintf(authority, sizeof(authority), "%s:%d", host, 443);
// backstop: never let a stray CR/LF in the host smuggle a second line into
// the CONNECT request (the host is already sanitized upstream)
{
const char *c;
for (c = authority; *c != '\0'; c++) {
if ((unsigned char) *c < ' ') {
strcpybuff(retour->msg, "proxy CONNECT: invalid host");
return 0;
}
}
}
snprintf(req, sizeof(req), "CONNECT %s HTTP/1.0" H_CRLF "Host: %s" H_CRLF,
authority, authority);
// creds go on the CONNECT, not the tunneled origin request
if (link_has_authorization(retour->req.proxy.name)) {
const char *a = jump_identification_const(retour->req.proxy.name);
const char *astart = jump_protocol_const(retour->req.proxy.name);
char autorisation[1100];
char user_pass[256];
autorisation[0] = user_pass[0] = '\0';
strncatbuff(user_pass, astart, (int) (a - astart) - 1);
strcpybuff(user_pass, unescape_http(OPT_GET_BUFF(opt),
OPT_GET_BUFF_SIZE(opt), user_pass));
code64((unsigned char *) user_pass, (int) strlen(user_pass),
(unsigned char *) autorisation, 0);
strlcatbuff(req, "Proxy-Authorization: Basic ", sizeof(req));
strlcatbuff(req, autorisation, sizeof(req));
strlcatbuff(req, H_CRLF, sizeof(req));
}
strlcatbuff(req, H_CRLF, sizeof(req)); // end of request headers
// raw send: ssl is set, so sendc() would route to TLS
{
const char *p = req;
size_t remain = strlen(req);
int stalls = 0;
while (remain > 0) {
const int n = (int) send(soc, p, (int) remain, 0);
if (n > 0) {
p += n;
remain -= (size_t) n;
stalls = 0;
} else {
#ifdef _WIN32
const int wouldblock = (WSAGetLastError() == WSAEWOULDBLOCK);
#else
const int wouldblock =
(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR);
#endif
// don't spin forever on a fatal error or an unwritable socket
if (!wouldblock || !check_writeinput_t(soc, timeout) ||
++stalls > 100) {
strcpybuff(retour->msg, "proxy CONNECT: write error");
return 0;
}
}
}
}
// proxy status line: "HTTP/1.x <code> ..."
if (proxy_getline(soc, line, sizeof(line), timeout) < 0) {
strcpybuff(retour->msg, "proxy CONNECT: no response");
return 0;
}
if (sscanf(line, "HTTP/%*d.%*d %d", &code) < 1)
code = 0;
if (code < 200 || code >= 300) {
snprintf(retour->msg, sizeof(retour->msg), "proxy CONNECT refused: %s",
strnotempty(line) ? line : "(no status)");
return 0;
}
// drain headers to the blank line; cap the count so a flooding proxy can't
// stall the crawl
{
int headers = 0;
for (;;) {
const int n = proxy_getline(soc, line, sizeof(line), timeout);
if (n < 0) {
strcpybuff(retour->msg, "proxy CONNECT: truncated response");
return 0;
}
if (n == 0)
break; // blank line: tunnel ready
if (++headers > 64) {
strcpybuff(retour->msg, "proxy CONNECT: too many response headers");
return 0;
}
}
}
return 1;
}
/* SOCKS5 client (RFC 1928, RFC 1929 auth), hostname mode only: the proxy
resolves the origin name (remote DNS, curl's socks5h). The stream is the
proxy socket, or a scripted buffer under -#test=socks5. */
#define SOCKS5_VERSION 0x05
#define SOCKS5_AUTH_VERSION 0x01
#define SOCKS5_METHOD_NONE 0x00
#define SOCKS5_METHOD_USERPASS 0x02
#define SOCKS5_METHOD_NOACCEPTABLE 0xFF
#define SOCKS5_CMD_CONNECT 0x01
#define SOCKS5_ATYP_IPV4 0x01
#define SOCKS5_ATYP_DOMAIN 0x03
#define SOCKS5_ATYP_IPV6 0x04
#define SOCKS5_MAXFIELD 255 /* one length byte: host, user and password */
typedef struct socks5_stream {
T_SOC soc; /* INVALID_SOCKET when scripted (self-test) */
int timeout;
socks5_test_io *io;
} socks5_stream;
static int socks5_fail(char *msg, size_t msgsize, const char *text) {
if (msgsize != 0)
strlcpybuff(msg, text, msgsize);
return 0;
}
/* Read exactly n bytes, or fail: SOCKS frames are not self-delimiting, so a
short read would desync the stream shared with the origin traffic. */
static int socks5_read_n(socks5_stream *st, unsigned char *buf, size_t n) {
size_t got = 0;
int stalls = 0;
if (st->soc == INVALID_SOCKET) { /* scripted */
socks5_test_io *const io = st->io;
if (n > io->reply_len - io->consumed)
return 0;
memcpy(buf, io->reply + io->consumed, n);
io->consumed += n;
return 1;
}
while (got < n) {
const int r = (int) recv(st->soc, (char *) buf + got, (int) (n - got), 0);
if (r > 0) {
got += (size_t) r;
stalls = 0;
} else if (r == 0) {
return 0; // proxy closed mid-frame
} else {
#ifdef _WIN32
const int wouldblock = (WSAGetLastError() == WSAEWOULDBLOCK);
#else
const int wouldblock =
(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR);
#endif
if (!wouldblock || !check_readinput_t(st->soc, st->timeout) ||
++stalls > 100)
return 0;
}
}
return 1;
}
static int socks5_write_all(socks5_stream *st, const unsigned char *buf,
size_t len) {
size_t remain = len;
int stalls = 0;
if (st->soc == INVALID_SOCKET) { /* scripted */
socks5_test_io *const io = st->io;
if (len > sizeof(io->sent) - io->sent_len)
return 0;
memcpy(io->sent + io->sent_len, buf, len);
io->sent_len += len;
return 1;
}
while (remain > 0) {
// raw send: the socket is still plain here, sendc() would route to TLS
const int n = (int) send(st->soc, (const char *) buf, (int) remain, 0);
if (n > 0) {
buf += n;
remain -= (size_t) n;
stalls = 0;
} else {
#ifdef _WIN32
const int wouldblock = (WSAGetLastError() == WSAEWOULDBLOCK);
#else
const int wouldblock =
(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR);
#endif
if (!wouldblock || !check_writeinput_t(st->soc, st->timeout) ||
++stalls > 100)
return 0;
}
}
return 1;
}
static const char *socks5_rep_message(unsigned char rep) {
switch (rep) {
case 0x01:
return "general failure";
case 0x02:
return "connection not allowed by ruleset";
case 0x03:
return "network unreachable";
case 0x04:
return "host unreachable";
case 0x05:
return "connection refused";
case 0x06:
return "TTL expired";
case 0x07:
return "command not supported";
case 0x08:
return "address type not supported";
default:
return "unknown error";
}
}
/* The reply's BND.ADDR is variable-length: consume exactly what ATYP says, or
the leftover bytes prepend to the first origin (or TLS) read. */
static int socks5_read_reply(socks5_stream *st, char *msg, size_t msgsize) {
unsigned char head[4];
unsigned char addr[SOCKS5_MAXFIELD + 2];
size_t addrlen;
if (!socks5_read_n(st, head, sizeof(head)))
return socks5_fail(msg, msgsize, "SOCKS5: no reply from proxy");
if (head[0] != SOCKS5_VERSION)
return socks5_fail(msg, msgsize, "SOCKS5: bad version in reply");
if (head[1] != 0x00) {
snprintf(msg, msgsize, "SOCKS5 connect failed: %s",
socks5_rep_message(head[1]));
return 0;
}
switch (head[3]) {
case SOCKS5_ATYP_IPV4:
addrlen = 4;
break;
case SOCKS5_ATYP_IPV6:
addrlen = 16;
break;
case SOCKS5_ATYP_DOMAIN: {
unsigned char len;
if (!socks5_read_n(st, &len, 1))
return socks5_fail(msg, msgsize, "SOCKS5: truncated reply");
addrlen = len; // <= 255, always fits addr[]
break;
}
default: // unknown length: the stream cannot be resynchronized
return socks5_fail(msg, msgsize, "SOCKS5: unknown address type in reply");
}
if (addrlen != 0 && !socks5_read_n(st, addr, addrlen))
return socks5_fail(msg, msgsize, "SOCKS5: truncated reply address");
if (!socks5_read_n(st, addr, 2)) // BND.PORT, unused
return socks5_fail(msg, msgsize, "SOCKS5: truncated reply port");
return 1;
}
static int socks5_negotiate(socks5_stream *st, const char *host, size_t hostlen,
int port, const char *user, size_t userlen,
const char *pass, size_t passlen, int want_auth,
char *msg, size_t msgsize) {
unsigned char frame[3 + SOCKS5_MAXFIELD + SOCKS5_MAXFIELD];
unsigned char rep[2];
/* greeting */
frame[0] = SOCKS5_VERSION;
frame[1] = (unsigned char) (want_auth ? 2 : 1);
frame[2] = SOCKS5_METHOD_NONE;
frame[3] = SOCKS5_METHOD_USERPASS;
if (!socks5_write_all(st, frame, (size_t) 2 + frame[1]))
return socks5_fail(msg, msgsize, "SOCKS5: write error");
if (!socks5_read_n(st, rep, sizeof(rep)))
return socks5_fail(msg, msgsize, "SOCKS5: no method reply from proxy");
if (rep[0] != SOCKS5_VERSION)
return socks5_fail(msg, msgsize, "SOCKS5: bad version in method reply");
switch (rep[1]) {
case SOCKS5_METHOD_NONE:
break;
case SOCKS5_METHOD_USERPASS:
if (!want_auth)
return socks5_fail(msg, msgsize,
"SOCKS5: proxy requires authentication, none given");
/* RFC 1929 sub-negotiation; its version byte is 0x01, not 0x05 */
frame[0] = SOCKS5_AUTH_VERSION;
frame[1] = (unsigned char) userlen;
memcpy(frame + 2, user, userlen);
frame[2 + userlen] = (unsigned char) passlen;
memcpy(frame + 3 + userlen, pass, passlen);
if (!socks5_write_all(st, frame, 3 + userlen + passlen))
return socks5_fail(msg, msgsize, "SOCKS5: write error");
if (!socks5_read_n(st, rep, sizeof(rep)))
return socks5_fail(msg, msgsize, "SOCKS5: no authentication reply");
if (rep[1] != 0x00)
return socks5_fail(msg, msgsize, "SOCKS5: authentication failed");
break;
case SOCKS5_METHOD_NOACCEPTABLE:
return socks5_fail(
msg, msgsize,
"SOCKS5: proxy accepts no authentication method we offer");
default:
return socks5_fail(msg, msgsize,
"SOCKS5: proxy selected an unknown method");
}
/* CONNECT to the origin, by name */
frame[0] = SOCKS5_VERSION;
frame[1] = SOCKS5_CMD_CONNECT;
frame[2] = 0x00; // RSV
frame[3] = SOCKS5_ATYP_DOMAIN;
frame[4] = (unsigned char) hostlen;
memcpy(frame + 5, host, hostlen);
frame[5 + hostlen] = (unsigned char) (port >> 8);
frame[6 + hostlen] = (unsigned char) (port & 0xFF);
if (!socks5_write_all(st, frame, 7 + hostlen))
return socks5_fail(msg, msgsize, "SOCKS5: write error");
return socks5_read_reply(st, msg, msgsize);
}
/* Decode the proxy userinfo into the two RFC 1929 fields. Split on the first
colon of the still-escaped string, so a %3A stays inside the username. */
static int socks5_credentials(httrackp *opt, const char *proxy_name, char *user,
size_t user_size, size_t *userlen, char *pass,
size_t pass_size, size_t *passlen, char *msg,
size_t msgsize) {
const char *const a = jump_identification_const(proxy_name); // past the '@'
const char *const astart = jump_protocol_const(proxy_name);
char userinfo[1024];
char *colon;
if (a <= astart || (size_t) (a - astart) - 1 >= sizeof(userinfo))
return socks5_fail(msg, msgsize, "SOCKS5: credentials too long");
userinfo[0] = '\0';
strncatbuff(userinfo, astart, (int) (a - astart) - 1);
colon = strchr(userinfo, ':');
if (colon != NULL)
*colon++ = '\0';
strlcpybuff(
user, unescape_http(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), userinfo),
user_size);
strlcpybuff(pass,
colon != NULL ? unescape_http(OPT_GET_BUFF(opt),
OPT_GET_BUFF_SIZE(opt), colon)
: "",
pass_size);
*userlen = strlen(user);
*passlen = strlen(pass);
// reject, never truncate: a clipped secret would authenticate as another one
if (*userlen > SOCKS5_MAXFIELD || *passlen > SOCKS5_MAXFIELD)
return socks5_fail(msg, msgsize, "SOCKS5: credentials too long");
if (*userlen == 0)
return socks5_fail(msg, msgsize, "SOCKS5: empty proxy username");
return 1;
}
static int socks5_handshake_stream(httrackp *opt, socks5_stream *st,
const char *adr, const char *proxy_name,
int ssl, char *msg, size_t msgsize) {
const char *const host = jump_identification_const(adr);
const char *const portsep = jump_toport_const(adr);
const size_t hostlen =
portsep != NULL ? (size_t) (portsep - host) : strlen(host);
// sized for the whole userinfo: decoding shrinks, so an over-long credential
// lands intact and is rejected below rather than silently clipped
char user[1024];
char pass[1024];
size_t userlen = 0, passlen = 0;
int want_auth = 0;
int port = ssl ? 443 : 80;
size_t i;
if (hostlen == 0 || hostlen > SOCKS5_MAXFIELD)
return socks5_fail(msg, msgsize, "SOCKS5: invalid origin host");
if (host[0] == '[') // ATYP=domain cannot carry an IPv6 literal
return socks5_fail(msg, msgsize,
"SOCKS5: IPv6 literal origin is not supported");
for (i = 0; i < hostlen; i++) {
if ((unsigned char) host[i] < ' ')
return socks5_fail(msg, msgsize, "SOCKS5: invalid origin host");
}
if (portsep != NULL) {
int p = -1;
sscanf(portsep + 1, "%d", &p);
if (p <= 0 || p > 65535)
return socks5_fail(msg, msgsize, "SOCKS5: invalid origin port");
port = p;
}
if (link_has_authorization(proxy_name)) {
if (!socks5_credentials(opt, proxy_name, user, sizeof(user), &userlen, pass,
sizeof(pass), &passlen, msg, msgsize))
return 0;
want_auth = 1;
}
return socks5_negotiate(st, host, hostlen, port, user, userlen, pass, passlen,
want_auth, msg, msgsize);
}
int socks5_handshake(httrackp *opt, htsblk *retour, const char *adr,
int timeout) {
socks5_stream st;
#if HTS_USEOPENSSL
const int ssl = retour->ssl;
#else
const int ssl = 0;
#endif
if (retour->soc == INVALID_SOCKET)
return 0;
st.soc = retour->soc;
st.timeout = timeout;
st.io = NULL;
return socks5_handshake_stream(opt, &st, adr, retour->req.proxy.name, ssl,
retour->msg, sizeof(retour->msg));
}
int socks5_handshake_scripted(httrackp *opt, const char *adr,
const char *proxy_name, socks5_test_io *io) {
socks5_stream st;
st.soc = INVALID_SOCKET;
st.timeout = 0;
st.io = io;
io->consumed = 0;
io->sent_len = 0;
io->msg[0] = '\0';
return socks5_handshake_stream(opt, &st, adr, proxy_name, 0, io->msg,
sizeof(io->msg));
}

82
src/htsproxy.h Normal file
View File

@@ -0,0 +1,82 @@
/* ------------------------------------------------------------ */
/*
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 2026 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/>.
Ethical use: we kindly ask that you NOT use this software to harvest email
addresses or to collect any other private information about people. Doing so
would dishonor our work and waste the many hours we have spent on it.
Please visit our Website: http://www.httrack.com
*/
/* ------------------------------------------------------------ */
/* File: Proxy tunneling (HTTP CONNECT, SOCKS5) .h */
/* Author: Xavier Roche */
/* ------------------------------------------------------------ */
#ifndef HTS_DEFPROXY
#define HTS_DEFPROXY
#include "htsglobal.h"
/* Forward definitions */
#ifndef HTS_DEF_FWSTRUCT_httrackp
#define HTS_DEF_FWSTRUCT_httrackp
typedef struct httrackp httrackp;
#endif
#ifndef HTS_DEF_FWSTRUCT_htsblk
#define HTS_DEF_FWSTRUCT_htsblk
typedef struct htsblk htsblk;
#endif
/* Open an HTTP CONNECT tunnel through the active proxy for an https request:
`retour->soc` must already be TCP-connected to the proxy, and `adr` is the
origin authority (url_adr, e.g. "https://host:port"). Sends the CONNECT
request (with Proxy-Authorization when the proxy carries credentials) and
reads the proxy's status line, so the caller's TLS handshake then runs
end-to-end with the origin. Blocks up to `timeout` seconds. Returns 1 on a
2xx tunnel, 0 on failure (retour->msg/statuscode set). */
int http_proxy_tunnel(httrackp *opt, htsblk *retour, const char *adr,
int timeout);
/* SOCKS5 (RFC 1928, RFC 1929 auth) handshake on the raw proxy socket, before
any TLS: `retour->soc` must be TCP-connected to the proxy and `adr` is the
origin (url_adr). The origin name is sent verbatim (ATYP=domain), so the
proxy resolves it; a bracketed IPv6 literal origin is rejected. Blocks up to
`timeout` seconds. Returns 1 with the stream positioned on the first origin
byte, 0 on failure (retour->msg set). */
int socks5_handshake(httrackp *opt, htsblk *retour, const char *adr,
int timeout);
/* Self-test hook (-#test=socks5): run the handshake against `reply`, a scripted
server byte stream, instead of a socket. `sent` captures the client frames,
`consumed` the reply bytes eaten (the frame-desync guard). */
typedef struct socks5_test_io {
const unsigned char *reply;
size_t reply_len;
size_t consumed;
unsigned char sent[1024];
size_t sent_len;
char msg[256];
} socks5_test_io;
int socks5_handshake_scripted(httrackp *opt, const char *adr,
const char *proxy_name, socks5_test_io *io);
#endif

View File

@@ -55,6 +55,7 @@ Please visit our Website: http://www.httrack.com
#include "htsmd5.h"
#include "htssniff.h"
#include "htscodec.h"
#include "htsproxy.h"
#if HTS_USEZLIB
#include "htszlib.h"
#endif
@@ -64,6 +65,7 @@ Please visit our Website: http://www.httrack.com
#include "coucal/coucal.h"
#include <ctype.h>
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
@@ -71,6 +73,9 @@ Please visit our Website: http://www.httrack.com
#ifndef _WIN32
#include <sys/socket.h>
#include <unistd.h>
#else
#include <io.h> /* _get_osfhandle, for the sparse-file hint */
#include <winioctl.h> /* FSCTL_SET_SPARSE */
#endif
/* very minimalistic internal tests */
@@ -752,7 +757,7 @@ static int st_filterbounds(httrackp *opt, int argc, char **argv) {
const size_t subjlen = 2048;
char *subj = malloct(big + 1);
char *pat = malloct(2 * stars + 2);
size_t steps = 0, maxsteps = 0, i;
size_t steps = 0, maxsteps = 0, depth = 0, maxdepth = 0, i;
(void) opt;
(void) argc;
@@ -773,8 +778,11 @@ static int st_filterbounds(httrackp *opt, int argc, char **argv) {
subj[subjlen] = '\0';
/* Budget must fire and hold: steps > cap (deleting the budget zeroes the
counter that is the enforcement), steps < 10*cap (unbudgeted ~1.26e9). */
assertf(strjoker_steps(subj, pat, &steps, &maxsteps) == NULL);
assertf(strjoker_bounds(subj, pat, &steps, &maxsteps, &depth, &maxdepth) ==
NULL);
assertf(steps > maxsteps && steps < 10 * maxsteps);
/* Depth caps the stack: uncapped this recurses 2046 frames, ~900KB (#574). */
assertf(depth == maxdepth);
assertf(strjokerfind(subj, pat) == NULL);
freet(pat);
freet(subj);
@@ -812,7 +820,11 @@ static int st_mime(httrackp *opt, int argc, char **argv) {
return 0;
}
static size_t st_decode_body(const char *arg, char *buf, size_t size);
static int st_charset(httrackp *opt, int argc, char **argv) {
char buf[512];
size_t len;
char *s;
(void) opt;
@@ -820,7 +832,8 @@ static int st_charset(httrackp *opt, int argc, char **argv) {
fprintf(stderr, "charset: needs a charset and a string\n");
return 1;
}
s = hts_convertStringToUTF8(argv[1], strlen(argv[1]), argv[0]);
len = st_decode_body(argv[1], buf, sizeof(buf));
s = hts_convertStringToUTF8(buf, len, argv[0]);
if (s != NULL) {
printf("%s\n", s);
freet(s);
@@ -845,16 +858,22 @@ static int st_metacharset(httrackp *opt, int argc, char **argv) {
}
static int st_isutf8(httrackp *opt, int argc, char **argv) {
char buf[512];
size_t len;
(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);
len = st_decode_body(argv[0], buf, sizeof(buf));
printf("%d\n", hts_isStringUTF8(buf, len) ? 1 : 0);
return 0;
}
static int st_idna_encode(httrackp *opt, int argc, char **argv) {
char buf[512];
size_t len;
char *s;
(void) opt;
@@ -862,7 +881,8 @@ static int st_idna_encode(httrackp *opt, int argc, char **argv) {
fprintf(stderr, "idna-encode: needs a hostname\n");
return 1;
}
s = hts_convertStringUTF8ToIDNA(argv[0], strlen(argv[0]));
len = st_decode_body(argv[0], buf, sizeof(buf));
s = hts_convertStringUTF8ToIDNA(buf, len);
if (s != NULL) {
printf("%s\n", s);
freet(s);
@@ -1024,7 +1044,7 @@ static int st_hashtable(httrackp *opt, int argc, char **argv) {
/* produce random patterns, or read from a file */
if (sscanf(snum, "%lu", &count) != 1) {
const off_t size = fsize(snum);
const LLint size = fsize(snum);
FILE *fp = fopen(snum, "rb");
if (fp != NULL) {
buff = malloct(size);
@@ -1124,7 +1144,10 @@ static int st_strsafe(httrackp *opt, int argc, char **argv) {
comes from argv so its length is opaque to the compiler (no static
-Wstringop-overflow, genuine runtime check). "overflow-buff" exercises
htsbuff. */
char small[4];
/* Not sizeof(char*): on ILP32 a char[4] equals the pointer size, and the
MSVC array-vs-pointer heuristic (sizeof(A) != sizeof(char*)) then reads
it as a pointer and silently skips the bound. */
char small[6];
const char *const src = (argc >= 2) ? argv[1] : "overflowing";
if (strcmp(argv[0], "overflow-buff") == 0) {
@@ -1608,7 +1631,8 @@ static int st_crange(httrackp *opt, int argc, char **argv) {
return 0;
}
/* Decode a body argument ("hex:FFD8.." or literal text) into buf. */
/* Decode an argument ("hex:FFD8.." or literal text) into buf. Raw non-UTF-8
bytes cannot survive a Windows command line, so hostile inputs go as hex. */
static size_t st_decode_body(const char *arg, char *buf, size_t size) {
size_t n = 0;
@@ -1648,6 +1672,94 @@ static int st_sniff(httrackp *opt, int argc, char **argv) {
return 0;
}
/* fsize()/fsize_utf8()/fpsize() must report a size past 4GB: 32-bit wraps both
ways there (MSVC's off_t and struct _stat st_size are long, 32-bit even on
x64), and a size under 4GB would survive an *unsigned* 32-bit truncation. */
static int st_fsize(httrackp *opt, int argc, char **argv) {
const LLint expected = 5 * 1024 * 1024 * 1024LL;
char BIGSTK path[HTS_URLMAXSIZE * 2];
char BIGSTK absent[HTS_URLMAXSIZE * 2];
/* both variants: only fsize() feeds the >2GB readers (file://, --list) */
const int width = (int) sizeof(fsize(""));
const int width_utf8 = (int) sizeof(fsize_utf8(""));
FILE *fp;
LLint got, got_utf8, gotp, gone;
int rc = 0;
(void) opt;
if (argc < 1) {
fprintf(stderr, "fsize: needs a directory\n");
return 1;
}
concat(path, sizeof(path), argv[0], "/sparse-5g.bin");
concat(absent, sizeof(absent), argv[0], "/no-such-file.bin");
/* sparse: seek past 4GB and write the last byte */
fp = FOPEN(path, "wb");
if (fp == NULL) {
fprintf(stderr, "fsize: cannot create '%s': %s\n", path, strerror(errno));
return 1;
}
#ifdef _WIN32
{
/* NTFS allocates the hole unless asked not to; POSIX gives it for free.
Best-effort: a non-sparse file still measures the same, just costs 5GB.
*/
HANDLE h = (HANDLE) _get_osfhandle(_fileno(fp));
DWORD ret;
if (h != INVALID_HANDLE_VALUE)
(void) DeviceIoControl(h, FSCTL_SET_SPARSE, NULL, 0, NULL, 0, &ret, NULL);
}
#endif
if (fseeko(fp, expected - 1, SEEK_SET) != 0 || fputc(0, fp) == EOF ||
fclose(fp) != 0) {
fprintf(stderr, "fsize: cannot extend '%s' to " LLintP ": %s\n", path,
expected, strerror(errno));
UNLINK(path);
return 1;
}
got = fsize(path);
got_utf8 = fsize_utf8(path);
fp = FOPEN(path, "rb");
if (fp == NULL) {
fprintf(stderr, "fsize: cannot reopen '%s': %s\n", path, strerror(errno));
gotp = -1;
} else {
gotp = fpsize(fp);
fclose(fp);
}
UNLINK(path);
gone = fsize(absent); /* contract: -1, not 0, when absent */
printf("fsize: width=%d,%d size=" LLintP "," LLintP " psize=" LLintP
" absent=" LLintP "\n",
width, width_utf8, got, got_utf8, gotp, gone);
if (width != 8 || width_utf8 != 8) {
fprintf(stderr, "fsize: return types are %d/%d bytes, expected 8\n", width,
width_utf8);
rc = 1;
}
if (got != expected || got_utf8 != expected) {
fprintf(stderr,
"fsize: fsize/fsize_utf8 are " LLintP "/" LLintP
", expected " LLintP "\n",
got, got_utf8, expected);
rc = 1;
}
if (gotp != expected) {
fprintf(stderr, "fsize: fpsize is " LLintP ", expected " LLintP "\n", gotp,
expected);
rc = 1;
}
if (gone != -1) {
fprintf(stderr, "fsize: absent file is " LLintP ", expected -1\n", gone);
rc = 1;
}
return rc;
}
static int st_savename(httrackp *opt, int argc, char **argv) {
lien_adrfilsave afs;
cache_back cache;
@@ -2794,11 +2906,43 @@ static int st_robots(httrackp *opt, int argc, char **argv) {
return 0;
}
/* Connected stream pair over loopback; Windows has no socketpair(). */
static int st_socketpair(T_SOC sv[2]) {
struct sockaddr_in sa;
socklen_t len = sizeof(sa);
T_SOC srv, cli, acc;
memset(&sa, 0, sizeof(sa));
sa.sin_family = AF_INET;
sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if ((srv = (T_SOC) socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
return -1;
if (bind(srv, (struct sockaddr *) &sa, sizeof(sa)) != 0 ||
listen(srv, 1) != 0 ||
getsockname(srv, (struct sockaddr *) &sa, &len) != 0) {
deletesoc(srv);
return -1;
}
if ((cli = (T_SOC) socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
deletesoc(srv);
return -1;
}
if (connect(cli, (struct sockaddr *) &sa, sizeof(sa)) != 0 ||
(acc = (T_SOC) accept(srv, NULL, NULL)) == INVALID_SOCKET) {
deletesoc(cli);
deletesoc(srv);
return -1;
}
deletesoc(srv);
sv[0] = acc;
sv[1] = cli;
return 0;
}
/* get_ftp_line must bound a hostile, CRLF-less reply into its internal
1024-byte buffer; ASan turns the pre-fix overflow into an abort here. */
#ifndef _WIN32
static int st_ftpline(httrackp *opt, int argc, char **argv) {
int sv[2];
T_SOC sv[2];
char line[2048];
char flood[4096];
@@ -2806,19 +2950,19 @@ static int st_ftpline(httrackp *opt, int argc, char **argv) {
(void) argc;
(void) argv;
memset(flood, 'x', sizeof(flood));
assertf(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0);
assertf(write(sv[1], "220 ", 4) == 4); // valid 3-digit code
assertf(write(sv[1], flood, sizeof(flood)) == (ssize_t) sizeof(flood));
assertf(write(sv[1], "\r\n", 2) == 2); // end the line so we return
close(sv[1]);
assertf(st_socketpair(sv) == 0);
// the 4102-byte reply fits the loopback send buffer, so no reader is needed
assertf(send(sv[1], "220 ", 4, 0) == 4); // valid 3-digit code
assertf(send(sv[1], flood, (int) sizeof(flood), 0) == (int) sizeof(flood));
assertf(send(sv[1], "\r\n", 2, 0) == 2); // end the line so we return
deletesoc(sv[1]);
line[0] = '\0';
get_ftp_line(sv[0], line, sizeof(line), 5);
close(sv[0]);
deletesoc(sv[0]);
printf("ftp-line self-test OK (bounded %d-byte reply)\n",
(int) sizeof(flood));
return 0;
}
#endif
/* ftp_split_userpass: well-formed split, plus a hostile over-long userinfo
that pre-fix overran user[256]/pass[256]. */
@@ -2890,11 +3034,11 @@ static const struct selftest_entry {
{"redirect-samefile", "", "same-file redirect detection self-test (#159)",
st_redirect_samefile},
{"mime", "<filename>", "MIME type for a filename", st_mime},
{"charset", "<charset> <string>",
{"charset", "<charset> <hex:..|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},
{"isutf8", "<hex:..|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",
@@ -2931,6 +3075,7 @@ static const struct selftest_entry {
"local save-name for a URL", st_savename},
{"sniff", "<content-type> <hex:..|text>", "MIME magic consistency",
st_sniff},
{"fsize", "<dir>", "file size past the 2GB signed-32-bit wrap", st_fsize},
{"cache", "<dir>", "cache read/write round-trip self-test", st_cache},
{"cacheindex", "", "cache-index (.ndx) parse must stay in bounds",
st_cacheindex},
@@ -2961,10 +3106,8 @@ static const struct selftest_entry {
st_contentcodings},
{"robots", "", "robots.txt RFC 9309 Allow/Disallow precedence self-test",
st_robots},
#ifndef _WIN32
{"ftp-line", "", "get_ftp_line bounds a hostile FTP reply line",
st_ftpline},
#endif
{"ftp-userpass", "", "ftp_split_userpass bounds URL userinfo", st_ftpuser},
};

View File

@@ -743,7 +743,8 @@ HTSEXT_API hts_boolean hts_findissystem(find_handle find);
HTSEXT_API FILE *hts_fopen_utf8(const char *path, const char *mode);
#define STAT hts_stat_utf8
typedef struct _stat STRUCT_STAT;
/* _stat64: _stat's st_size is a 32-bit long, even in x64 builds */
typedef struct _stat64 STRUCT_STAT;
HTSEXT_API int hts_stat_utf8(const char *path, STRUCT_STAT *buf);

View File

@@ -45,6 +45,7 @@ Please visit our Website: http://www.httrack.com
#include "htsdefines.h"
#include "httrack.h"
#include "htslib.h"
#include "htscharset.h" // after htslib.h: winsock2.h must precede windows.h
/* Static definitions */
static int fexist(const char *s);
@@ -206,6 +207,7 @@ int main(int argc, char **argv) {
httrackp *opt;
#ifdef _WIN32
hts_argv_utf8(&argc, &argv);
{
WORD wVersionRequested; // requested version WinSock API
WSADATA wsadata; // Windows Sockets API data

View File

@@ -70,7 +70,7 @@
<Link>
<!-- vcpkg auto-links libssl/libcrypto/zlib/brotli/zstd; only the OS import
lib is explicit. -->
<AdditionalDependencies>Ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>Ws2_32.lib;Shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
@@ -125,6 +125,7 @@
<ClCompile Include="htsmodules.c" />
<ClCompile Include="htsname.c" />
<ClCompile Include="htsparse.c" />
<ClCompile Include="htsproxy.c" />
<ClCompile Include="htsrobots.c" />
<ClCompile Include="htsselftest.c" />
<ClCompile Include="htssniff.c" />

View File

@@ -577,9 +577,11 @@ PT_Index PT_LoadCache(const char *filename) {
chain = coucal_enum_next(&en);
}
if (chain != NULL) {
if (!link_has_authority(chain->name))
strcat(index->slots.common.startUrl, "http://");
strcat(index->slots.common.startUrl, chain->name);
const char *scheme = link_has_authority(chain->name) ? "" : "http://";
snprintf(index->slots.common.startUrl,
sizeof(index->slots.common.startUrl), "%s%s", scheme,
(const char *) chain->name);
}
}
}
@@ -632,7 +634,7 @@ int PT_EnumCache(PT_Indexes indexes,
const long int index_id = (long int) chain->value.intg;
const char *const url = chain->name;
if (index_id >= 0 && index_id <= indexes->index_size) {
if (index_id >= 0 && index_id < indexes->index_size) {
PT_Element item =
PT_ReadCache(indexes->index[index_id], url,
FETCH_HEADERS | FETCH_BODY);
@@ -757,7 +759,7 @@ PT_Element PT_ReadIndex(PT_Indexes indexes, const char *url, int flags) {
if (strncmp(url, "http://", 7) == 0)
url += 7;
if (coucal_read(indexes->cil, url, &index_id)) {
if (index_id >= 0 && index_id <= indexes->index_size) {
if (index_id >= 0 && index_id < indexes->index_size) {
PT_Element item = PT_ReadCache(indexes->index[index_id], url, flags);
if (item != NULL) {
@@ -780,7 +782,7 @@ int PT_LookupIndex(PT_Indexes indexes, const char *url) {
if (strncmp(url, "http://", 7) == 0)
url += 7;
if (coucal_read(indexes->cil, url, &index_id)) {
if (index_id >= 0 && index_id <= indexes->index_size) {
if (index_id >= 0 && index_id < indexes->index_size) {
return 1;
} else {
proxytrack_print_log(CRITICAL,
@@ -967,10 +969,12 @@ int PT_LoadCache__New(PT_Index index_, const char *filename) {
/* First link as starting URL */
if (!firstSeen) {
if (strstr(filenameIndex, "/robots.txt") == NULL) {
const char *scheme =
link_has_authority(filenameIndex) ? "" : "http://";
firstSeen = 1;
if (!link_has_authority(filenameIndex))
strcat(index->startUrl, "http://");
strcat(index->startUrl, filenameIndex);
snprintf(index->startUrl, sizeof(index->startUrl), "%s%s",
scheme, filenameIndex);
}
}
} else {
@@ -1102,7 +1106,8 @@ static PT_Element PT_ReadCache__New_u(PT_Index index_, const char *url,
&& (!isalpha(previous_save_[0]) || previous_save_[1] != ':')) // c:/home/foo/bar.gif
) {
index->safeCache = 1;
sprintf(previous_save, "%s%s", index->path, previous_save_);
snprintf(previous_save, sizeof(previous_save), "%s%s",
index->path, previous_save_);
}
// bogus format (includes buggy absolute path)
else {
@@ -1127,15 +1132,16 @@ static PT_Element PT_ReadCache__New_u(PT_Index index_, const char *url,
int saveLen = (int) strlen(previous_save_);
if (index->fixedPath < saveLen) {
sprintf(previous_save, "%s%s", index->path,
previous_save_ + index->fixedPath);
snprintf(previous_save, sizeof(previous_save), "%s%s",
index->path, previous_save_ + index->fixedPath);
} else {
snprintf(r->msg, sizeof(r->msg), "Bogus fixePath prefix for %s (prefixLen=%d)",
previous_save_, (int) index->fixedPath);
r->statuscode = STATUSCODE_INVALID;
}
} else {
sprintf(previous_save, "%s%s", index->path, previous_save_);
snprintf(previous_save, sizeof(previous_save), "%s%s",
index->path, previous_save_);
}
}
}
@@ -1348,45 +1354,71 @@ static int PT_SaveCache__New(PT_Indexes indexes, const char *filename) {
/* Old HTTrack cache (dat/ndx) format */
/* ------------------------------------------------------------ */
static int cache_brstr(char *adr, char *s) {
static int cache_brstr(char *adr, char *s, size_t s_size) {
int i;
int off;
char buff[256 + 1];
char buff[256 + 4];
off = binput(adr, buff, 256);
adr += off;
sscanf(buff, "%d", &i);
if (i > 0)
strncpy(s, adr, i);
*(s + i) = '\0';
off += i;
/* binput stops at adr's terminating NUL; a value only follows a real line
terminator, so never step past end-of-buffer. */
if (adr[off - 1] == '\0') {
s[0] = '\0';
return off - 1;
}
/* an empty/non-numeric field leaves i unset: treat as length 0 */
if (sscanf(buff, "%d", &i) != 1 || i < 0 || i > 32768)
i = 0;
if (i > 0) {
/* a corrupt cache may declare a length past the buffer end; bound the copy
and the advance to the bytes actually present */
const size_t avail = strnlen(adr + off, (size_t) i);
const size_t store = avail < s_size ? avail : s_size - 1;
memcpy(s, adr + off, store);
s[store] = '\0';
off += (int) avail;
} else {
s[0] = '\0';
}
return off;
}
static void cache_rstr(FILE * fp, char *s) {
INTsys i;
static void cache_rstr(FILE *fp, char *s, size_t s_size) {
INTsys i = 0;
char buff[256 + 4];
linput(fp, buff, 256);
sscanf(buff, INTsysP, &i);
if (i < 0 || i > 32768) /* error, something nasty happened */
/* an unmatched sscanf leaves i untouched: length 0, not a stack value */
if (sscanf(buff, INTsysP, &i) != 1 || i < 0 || i > 32768)
i = 0;
if (i > 0) {
if ((int) fread(s, 1, i, fp) != i) {
/* store at most s_size-1 bytes, but consume all i bytes so the next field
stays aligned when a tampered cache declares more than the destination
holds */
const size_t want = (size_t) i;
const size_t store = want < s_size ? want : s_size - 1;
if (fread(s, 1, store, fp) != store) {
assertf(! "fread_cache_failed");
}
if (want > store && fseek(fp, (long) (want - store), SEEK_CUR) != 0) {
assertf(!"fseek_cache_failed");
}
s[store] = '\0';
} else {
s[0] = '\0';
}
*(s + i) = '\0';
}
static char *cache_rstr_addr(FILE * fp) {
INTsys i;
INTsys i = 0;
char *addr = NULL;
char buff[256 + 4];
linput(fp, buff, 256);
sscanf(buff, "%d", &i);
if (i < 0 || i > 32768) /* error, something nasty happened */
/* an unmatched sscanf leaves i untouched: length 0, not a stack value */
if (sscanf(buff, INTsysP, &i) != 1 || i < 0 || i > 32768)
i = 0;
if (i > 0) {
addr = malloc(i + 1);
@@ -1400,19 +1432,31 @@ static char *cache_rstr_addr(FILE * fp) {
return addr;
}
/* Allocate n+1 bytes for an n-byte cache body, rejecting a hostile/corrupt
length that would demand an absurd allocation or wrap n+1. NULL on refuse. */
static char *cache_alloc_body(size_t n) {
if (n >= (size_t) INT32_MAX)
return NULL;
return (char *) malloc(n + 1);
}
static void cache_rint(FILE * fp, int *i) {
char s[256];
cache_rstr(fp, s);
sscanf(s, "%d", i);
cache_rstr(fp, s, sizeof(s));
if (sscanf(s, "%d", i) != 1)
*i = 0;
}
static void cache_rLLint(FILE * fp, unsigned long *i) {
int l;
char s[256];
cache_rstr(fp, s);
sscanf(s, "%d", &l);
cache_rstr(fp, s, sizeof(s));
/* reject a non-numeric or negative field: a negative length cast to unsigned
would wrap a later malloc(size+1) to a tiny buffer */
if (sscanf(s, "%d", &l) != 1 || l < 0)
l = 0;
*i = (unsigned long) l;
}
@@ -1472,12 +1516,12 @@ static int PT_LoadCache__Old(PT_Index index_, const char *filename) {
char *a = use;
use[ndxSize] = '\0';
a += cache_brstr(a, firstline);
a += cache_brstr(a, firstline, sizeof(firstline));
if (strncmp(firstline, "CACHE-", 6) == 0) { // Nouvelle version du cache
if (strncmp(firstline, "CACHE-1.", 8) == 0) { // Version 1.1x
cache->version = (int) (firstline[8] - '0'); // cache 1.x
if (cache->version <= 5) {
a += cache_brstr(a, firstline);
a += cache_brstr(a, firstline, sizeof(firstline));
strcpy(cache->lastmodified, firstline);
} else {
fclose(cache->dat);
@@ -1523,11 +1567,12 @@ static int PT_LoadCache__Old(PT_Index index_, const char *filename) {
if (!firstSeen) {
if (strstr(line, "/robots.txt") == NULL) {
PT_Index__Old index = cache;
const char *scheme =
link_has_authority(line) ? "" : "http://";
firstSeen = 1;
if (!link_has_authority(line))
strcat(index->startUrl, "http://");
strcat(index->startUrl, line);
snprintf(index->startUrl, sizeof(index->startUrl), "%s%s",
scheme, line);
}
}
@@ -1623,9 +1668,13 @@ static PT_Element PT_ReadCache__Old_u(PT_Index index_, const char *url,
String urlDecoded;
r->statuscode = old_r.statuscode;
r->size = old_r.size; // taille fichier
strcpy(r->msg, old_r.msg);
strcpy(r->contenttype, old_r.contenttype);
r->size = old_r.size > 0 ? (size_t) old_r.size : 0; // taille fichier
/* these fixed fields come straight from fread and carry no guaranteed
NUL; terminate them before the bounded copy */
old_r.msg[sizeof(old_r.msg) - 1] = '\0';
old_r.contenttype[sizeof(old_r.contenttype) - 1] = '\0';
strcpybuff(r->msg, old_r.msg);
strcpybuff(r->contenttype, old_r.contenttype);
/* Guess the destination filename.. this sucks, because this method is not reliable.
Yes, the old 1.0 cache format was *that* bogus. /rx */
@@ -1666,26 +1715,27 @@ static PT_Element PT_ReadCache__Old_u(PT_Index index_, const char *url,
cache_rint(cache->dat, &r->statuscode);
cache_rLLint(cache->dat, &size_);
r->size = (size_t) size_;
cache_rstr(cache->dat, r->msg);
cache_rstr(cache->dat, r->contenttype);
cache_rstr(cache->dat, r->msg, sizeof(r->msg));
cache_rstr(cache->dat, r->contenttype, sizeof(r->contenttype));
if (cache->version >= 3)
cache_rstr(cache->dat, r->charset);
cache_rstr(cache->dat, r->lastmodified);
cache_rstr(cache->dat, r->etag);
cache_rstr(cache->dat, r->location);
cache_rstr(cache->dat, r->charset, sizeof(r->charset));
cache_rstr(cache->dat, r->lastmodified, sizeof(r->lastmodified));
cache_rstr(cache->dat, r->etag, sizeof(r->etag));
cache_rstr(cache->dat, r->location, sizeof(location_default));
if (cache->version >= 2)
cache_rstr(cache->dat, r->cdispo);
cache_rstr(cache->dat, r->cdispo, sizeof(r->cdispo));
if (cache->version >= 4) {
cache_rstr(cache->dat, previous_save_); // adr
cache_rstr(cache->dat, previous_save_); // fil
cache_rstr(cache->dat, previous_save_, sizeof(previous_save_)); // adr
cache_rstr(cache->dat, previous_save_, sizeof(previous_save_)); // fil
previous_save[0] = '\0';
cache_rstr(cache->dat, previous_save_); // save
cache_rstr(cache->dat, previous_save_,
sizeof(previous_save_)); // save
}
if (cache->version >= 5) {
r->headers = cache_rstr_addr(cache->dat);
}
//
cache_rstr(cache->dat, check);
cache_rstr(cache->dat, check, sizeof(check));
if (strcmp(check, "HTS") == 0) { /* intégrité OK */
ok = 1;
}
@@ -1714,7 +1764,8 @@ static PT_Element PT_ReadCache__Old_u(PT_Index index_, const char *url,
&& (!isalpha(previous_save_[0]) || previous_save_[1] != ':')) // c:/home/foo/bar.gif
) {
index->safeCache = 1;
sprintf(previous_save, "%s%s", index->path, previous_save_);
snprintf(previous_save, sizeof(previous_save), "%s%s", index->path,
previous_save_);
}
// bogus format (includes buggy absolute path)
else {
@@ -1739,15 +1790,16 @@ static PT_Element PT_ReadCache__Old_u(PT_Index index_, const char *url,
int saveLen = (int) strlen(previous_save_);
if (index->fixedPath < saveLen) {
sprintf(previous_save, "%s%s", index->path,
previous_save_ + index->fixedPath);
snprintf(previous_save, sizeof(previous_save), "%s%s",
index->path, previous_save_ + index->fixedPath);
} else {
snprintf(r->msg, sizeof(r->msg), "Bogus fixePath prefix for %s (prefixLen=%d)",
previous_save_, (int) index->fixedPath);
r->statuscode = STATUSCODE_INVALID;
}
} else {
sprintf(previous_save, "%s%s", index->path, previous_save_);
snprintf(previous_save, sizeof(previous_save), "%s%s",
index->path, previous_save_);
}
}
}
@@ -1764,7 +1816,7 @@ static PT_Element PT_ReadCache__Old_u(PT_Index index_, const char *url,
FILE *fp = fopen(previous_save, "rb");
if (fp != NULL) {
r->adr = (char *) malloc(r->size + 1);
r->adr = cache_alloc_body(r->size);
if (r->adr != NULL) {
if (r->size > 0 && fread(r->adr, 1, r->size, fp) != r->size) {
r->statuscode = STATUSCODE_INVALID;
@@ -1784,7 +1836,7 @@ static PT_Element PT_ReadCache__Old_u(PT_Index index_, const char *url,
} else {
// lire fichier (d'un coup)
if (flags & FETCH_BODY) {
r->adr = (char *) malloc(r->size + 1);
r->adr = cache_alloc_body(r->size);
if (r->adr != NULL) {
if (fread(r->adr, 1, r->size, cache->dat) != r->size) { // erreur
free(r->adr);

View File

@@ -23,7 +23,7 @@ conv 'iso-8859-1' 'café' 'café'
# windows-1252 is NOT latin-1: 0x80 is the euro sign, not U+0080. This is the
# case that actually exercises the cp1252 table (the 0x80-0x9F range).
conv 'windows-1252' $'\x80' '€'
conv 'windows-1252' 'hex:80' '€'
# pure ASCII is charset-invariant
conv 'us-ascii' 'hello' 'hello'
@@ -34,8 +34,8 @@ conv 'no-such-charset-xyz' 'abc' 'abc'
test "$(httrack -O /dev/null -#test=charset 'no-such-charset-xyz' 'café' 2>/dev/null)" == "" || exit 1
# malformed UTF-8 (lone continuation byte, truncated lead byte) must not crash
runs 'utf-8' $'\x80'
runs 'utf-8' $'\xc3'
runs 'utf-8' 'hex:80'
runs 'utf-8' 'hex:c3'
# hts_getCharsetFromMeta: <meta> charset extraction, HTML5 and legacy forms.
# -#test=metacharset <html> prints the charset, or "(none)".
@@ -89,11 +89,11 @@ utf8() {
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
utf8 'hex:636166e9' '0' # latin-1 accent
utf8 'hex:c3' '0' # truncated lead byte
utf8 'hex:a9' '0' # lone continuation byte
utf8 'hex:f09f9880' '1' # 4-byte emoji
utf8 'hex:c0af' '0' # overlong (latin-1 "À¯" must stay convertible)
utf8 'hex:eda080' '0' # UTF-16 surrogate
utf8 'hex:f4908080' '0' # above U+10FFFF
utf8 'hex:f888808080' '0' # legacy 5-byte form

View File

@@ -52,14 +52,15 @@ run "$tmp/dir" "$tmp/adir"
loghas 'Could not include URL list "[^"]+": not a regular file'
# unreadable regular file: the fopen() errno arm fires, distinct from the
# directory branch. Root bypasses mode 000, so skip it there.
if test "$(id -u)" -ne 0; then
: >"$tmp/noperm.txt"
chmod 000 "$tmp/noperm.txt"
# directory branch. Needs an OS that enforces mode 000: root bypasses it, and
# Windows ignores chmod entirely, so probe rather than assume.
: >"$tmp/noperm.txt"
chmod 000 "$tmp/noperm.txt"
if ! cat "$tmp/noperm.txt" >/dev/null 2>&1; then
run "$tmp/perm" "$tmp/noperm.txt"
chmod 644 "$tmp/noperm.txt"
loghas 'Could not include URL list "[^"]+": .+'
lognot 'not a regular file'
fi
chmod 644 "$tmp/noperm.txt"
exit 0

View File

@@ -193,5 +193,13 @@ test "$(with_timeout httrack -O /dev/null -#test=filter "$pat" "$subj")" == "$su
match '*[aX]X*[a]Y' 'aXaXaY'
nomatch '*[aX]X*[a]Y' 'aXbXaY'
# hostile patterns must not stack-overflow or hang: length + work caps
# hostile patterns must not stack-overflow or hang: length + depth + work caps
test "$(with_timeout httrack -O /dev/null -#test=filterbounds)" == "filterbounds: OK" || exit 1
# and the depth cap must hold on the 1MB stack a Windows thread gets (#574):
# uncapped, the same pattern recursed ~900KB deep. Skipped where 512K is too
# small to even start the binary.
if (ulimit -s 512 && httrack --version >/dev/null 2>&1); then
out=$(ulimit -s 512 && with_timeout httrack -O /dev/null -#test=filterbounds)
test "$out" == "filterbounds: OK" || exit 1
fi

View File

@@ -0,0 +1,18 @@
#!/bin/bash
#
# File sizes past the 32-bit wrap ('httrack -#test=fsize <dir>'). 5GB, so a
# signed *and* an unsigned truncation both show up; sparse, so it costs no disk.
set -euo pipefail
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
out=$(httrack -#test=fsize "$dir")
echo "$out"
want="fsize: width=8,8 size=5368709120,5368709120 psize=5368709120 absent=-1"
test "$out" == "$want" || {
echo "FAIL: unexpected size report (want '$want')"
exit 1
}

View File

@@ -38,4 +38,7 @@ dec 'xn--!!!' '㚯'
dec 'xn--already-encoded.com' 'ǮalǭǮreaǭǫdy.com'
# second label has a truncated UTF-8 sequence: used to leak the segment buffer.
enc_bad "$(printf 'b\xc3\xbcchev.\xe4\xbe\x8b\xe5\xad\x62\xc3\xbccheple')"
# Fed as hex: the raw bytes must reach the encoder intact, but invalid UTF-8
# cannot survive a Windows command line (it round-trips through UTF-16 to valid
# U+FFFD, which then encodes fine instead of being rejected).
enc_bad 'hex:62c3bc636865762ee4be8be5ad62c3bc636865706c65'

View File

@@ -32,6 +32,13 @@ esac
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_rcfile.XXXXXX") || exit 1
trap 'rm -rf "$tmp"' EXIT HUP INT QUIT PIPE TERM
# HTS_HTTRACKRC is ".httrackrc" on POSIX but "httrackrc" on Windows: write both,
# each platform reads the one it knows.
write_rc() {
cat >"$1/.httrackrc"
cp "$1/.httrackrc" "$1/httrackrc"
}
# --- 1. alias token survives the bound intact -------------------------------
d1="$tmp/intact"
mkdir -p "$d1"
@@ -40,7 +47,7 @@ echo '<html><body>hello</body></html>' >"$d1/index.html"
# optinclude_file() lowercases each config line, so the marker is lowercase to
# survive the comparison verbatim.
marker='zzz_rcfile_marker_0123456789_abcdefghijklmnopqrstuvwxyz_intact'
printf 'user-agent=%s\n' "$marker" >"$d1/.httrackrc"
printf 'user-agent=%s\n' "$marker" | write_rc "$d1"
# Run with no -O so the working-directory .httrackrc is loaded (an -O path makes
# the engine skip the rc files). Output lands in the temp dir. Guard the run so a
@@ -73,7 +80,7 @@ echo '<html><body>hi</body></html>' >"$d2/index.html"
val=$(printf 'a%.0s' $(seq 1 200))
for _ in $(seq 1 400); do
printf 'user-agent=%s\n' "$val"
done >"$d2/.httrackrc"
done | write_rc "$d2"
# The process aborts (httrack turns the fatal signal into exit 134 either way),
# so the exit code does not distinguish the bounded abort from a heap overflow;

View File

@@ -32,10 +32,10 @@ case "$err" in
esac
# Same guarantee for the htsbuff builder. The source is exactly the buffer
# capacity (4 bytes into a 4-byte buffer), so this also pins the boundary: a
# capacity (6 bytes into a 6-byte buffer), so this also pins the boundary: a
# '<=' off-by-one in the capacity check would let it through (and print "NOT
# aborted"). Match the specific htsbuff abort message, not just any assert.
err=$(httrack -#test=strsafe overflow-buff "abcd" 2>&1) || true
err=$(httrack -#test=strsafe overflow-buff "abcdef" 2>&1) || true
case "$err" in
*"strsafe: NOT aborted"*)
echo "htsbuff over-capacity write was NOT caught" >&2

View File

@@ -0,0 +1,21 @@
#!/bin/bash
# A corrupt proxytrack .ndx must not overflow the fixed cache-read buffers. The
# sanitizer CI build turns any regression here into a hard stack/heap overflow.
set -euo pipefail
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
# The first length-prefixed field declares far more than firstline[256] holds;
# cache_brstr must clamp the copy to the destination, not the declared length.
{
printf '4000\n'
head -c 4000 /dev/zero | tr '\0' 'A'
} >"$dir/foo.ndx"
: >"$dir/foo.dat" # cache_brstr runs only when the sibling .dat opens
proxytrack --convert "$dir/out.arc" "$dir/foo.ndx" >/dev/null 2>&1 || {
echo "FAIL: proxytrack crashed/errored on a corrupt cache index"
exit 1
}

View File

@@ -57,6 +57,7 @@ TESTS = \
01_engine-pause.test \
01_engine-rcfile.test \
01_engine-reconcile.test \
01_engine-fsize.test \
01_engine-redirect.test \
01_engine-relative.test \
01_engine-robots.test \
@@ -128,6 +129,7 @@ TESTS = \
49_local-cookiewall.test \
50_local-contentcodings.test \
51_local-update-codec.test \
52_local-socks5.test
52_local-socks5.test \
53_local-proxytrack-cache-corrupt.test
CLEANFILES = check-network_sh.cache