mirror of
https://github.com/xroche/httrack.git
synced 2026-07-17 06:10:42 +03:00
Compare commits
3 Commits
codeql-ign
...
proxy-conn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c40bc14cec | ||
|
|
29e868b2ca | ||
|
|
f1418bac3c |
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -46,6 +46,12 @@ jobs:
|
|||||||
# fopen's umask-controlled 0666 is intended for mirror/cache/log
|
# fopen's umask-controlled 0666 is intended for mirror/cache/log
|
||||||
# output; the one credential file (cookies.txt) is kept 0600 on Unix.
|
# output; the one credential file (cookies.txt) is kept 0600 on Unix.
|
||||||
config: |
|
config: |
|
||||||
|
paths-ignore:
|
||||||
|
# Demo callback samples, not part of libhttrack.
|
||||||
|
- libtest
|
||||||
|
# ProxyTrack: a separate legacy binary with no auth surface; its
|
||||||
|
# recv/cache-parse code trips taint queries by design.
|
||||||
|
- src/proxy
|
||||||
query-filters:
|
query-filters:
|
||||||
- exclude:
|
- exclude:
|
||||||
id: cpp/world-writable-file-creation
|
id: cpp/world-writable-file-creation
|
||||||
|
|||||||
@@ -2696,9 +2696,12 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
|
|||||||
int dispo = 0;
|
int dispo = 0;
|
||||||
|
|
||||||
// probe the resolved address list once per fresh connect (cache hit:
|
// probe the resolved address list once per fresh connect (cache hit:
|
||||||
// the host was resolved when this connect was opened)
|
// the host was resolved when this connect was opened). Not under a
|
||||||
|
// proxy: the socket dials the proxy, so resolving the origin here leaks
|
||||||
|
// its DNS and lets a proxy-connect failure fall back to dialing it
|
||||||
|
// direct.
|
||||||
if (cf->addr_count < 0 && back[i].r.soc != INVALID_SOCKET &&
|
if (cf->addr_count < 0 && back[i].r.soc != INVALID_SOCKET &&
|
||||||
!back[i].r.is_file) {
|
!back[i].r.is_file && !back[i].r.req.proxy.active) {
|
||||||
SOCaddr scratch[HTS_MAXADDRNUM];
|
SOCaddr scratch[HTS_MAXADDRNUM];
|
||||||
|
|
||||||
cf->addr_count = hts_dns_resolve_all(opt, back[i].url_adr, scratch,
|
cf->addr_count = hts_dns_resolve_all(opt, back[i].url_adr, scratch,
|
||||||
@@ -3310,16 +3313,26 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
|
|||||||
back[i].chunk_blocksize = -1; /* ending */
|
back[i].chunk_blocksize = -1; /* ending */
|
||||||
back[i].r.totalsize += chunk_size; // noter taille
|
back[i].r.totalsize += chunk_size; // noter taille
|
||||||
if (back[i].r.adr != NULL || !back[i].r.is_write) { // Not to disk
|
if (back[i].r.adr != NULL || !back[i].r.is_write) { // Not to disk
|
||||||
back[i].r.adr =
|
// totalsize sums attacker-declared chunk sizes; past
|
||||||
(char *) realloct(back[i].r.adr,
|
// 2GB the (size_t) cast below truncates on 32-bit and
|
||||||
(size_t) back[i].r.totalsize + 1);
|
// under-allocates. Mark the chunk invalid so the shared
|
||||||
if (!back[i].r.adr) {
|
// error path tears the transfer down.
|
||||||
if (cache->log != NULL) {
|
if (back[i].r.totalsize > INT32_MAX) {
|
||||||
hts_log_print(opt, LOG_ERROR,
|
hts_log_print(opt, LOG_WARNING,
|
||||||
"not enough memory (" LLintP
|
"Chunked resource too large for %s%s",
|
||||||
") for %s%s",
|
back[i].url_adr, back[i].url_fil);
|
||||||
(LLint) back[i].r.totalsize,
|
chunk_size = -1;
|
||||||
back[i].url_adr, back[i].url_fil);
|
} else {
|
||||||
|
back[i].r.adr = (char *) realloct(
|
||||||
|
back[i].r.adr, (size_t) back[i].r.totalsize + 1);
|
||||||
|
if (!back[i].r.adr) {
|
||||||
|
if (cache->log != NULL) {
|
||||||
|
hts_log_print(opt, LOG_ERROR,
|
||||||
|
"not enough memory (" LLintP
|
||||||
|
") for %s%s",
|
||||||
|
(LLint) back[i].r.totalsize,
|
||||||
|
back[i].url_adr, back[i].url_fil);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
tests/55_local-chunked.test
Executable file
18
tests/55_local-chunked.test
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# A well-formed Transfer-Encoding: chunked response mirrors intact: the chunk
|
||||||
|
# automaton (htsback.c) joins the bodies and the 2GB in-RAM cap stays quiet.
|
||||||
|
# The 76-char run spans a 64-byte chunk boundary (only contiguous once joined);
|
||||||
|
# a CR anywhere would be leftover framing; '</body>' proves no truncation.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${top_srcdir:=..}"
|
||||||
|
|
||||||
|
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 \
|
||||||
|
--found 'chunked/page.html' \
|
||||||
|
--file-matches 'chunked/page.html' \
|
||||||
|
'chunk-body chunk-body chunk-body chunk-body chunk-body chunk-body chunk-body' \
|
||||||
|
--file-matches 'chunked/page.html' '</body>' \
|
||||||
|
--file-not-matches 'chunked/page.html' $'\r' \
|
||||||
|
--log-not-found 'too large' \
|
||||||
|
httrack 'BASEURL/chunked/index.html'
|
||||||
93
tests/56_local-proxy-noleak.test
Normal file
93
tests/56_local-proxy-noleak.test
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# A crawl through a proxy must never resolve or dial the origin itself: the proxy
|
||||||
|
# does that. A dead proxy + a multi-address origin used to fall back to dialing
|
||||||
|
# the origin direct (bypassing the proxy, leaking its DNS and IP). The decoy
|
||||||
|
# origin here must therefore receive nothing.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${top_srcdir:=..}"
|
||||||
|
|
||||||
|
# shellcheck source=tests/testlib.sh
|
||||||
|
. "$top_srcdir/tests/testlib.sh"
|
||||||
|
|
||||||
|
if test "${V6_SUPPORT:-}" == "no"; then
|
||||||
|
echo "no IPv6 support (resolver override compiled out), skipping"
|
||||||
|
exit 77
|
||||||
|
fi
|
||||||
|
python=$(find_python) || {
|
||||||
|
echo "python3 missing, skipping"
|
||||||
|
exit 77
|
||||||
|
}
|
||||||
|
|
||||||
|
server=$(nativepath "$top_srcdir/tests/local-server.py")
|
||||||
|
root=$(nativepath "$top_srcdir/tests/server-root")
|
||||||
|
tmpdir=$(mktemp -d)
|
||||||
|
serverpid=
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
stop_server "$serverpid"
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# decoy origin: it must stay silent. LOCAL_SERVER_VERBOSE logs any request it gets.
|
||||||
|
LOCAL_SERVER_VERBOSE=1 "$python" "$server" --root "$root" --bind 127.0.0.1 \
|
||||||
|
>"$tmpdir/srv.out" 2>"$tmpdir/srv.err" &
|
||||||
|
serverpid=$!
|
||||||
|
port=
|
||||||
|
for _ in $(seq 1 50); do
|
||||||
|
line=$(head -n1 "$tmpdir/srv.out" 2>/dev/null || true)
|
||||||
|
if test "${line%% *}" == "PORT"; then
|
||||||
|
port="${line#PORT }"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
kill -0 "$serverpid" 2>/dev/null || {
|
||||||
|
echo "server exited early: $(cat "$tmpdir/srv.err")"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
test -n "$port" || {
|
||||||
|
echo "could not discover server port"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# a proxy port nothing listens on: grab a free one and let it close (refuses).
|
||||||
|
deadproxy=$("$python" -c \
|
||||||
|
'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()')
|
||||||
|
|
||||||
|
# origin resolves to two addresses so a fallback would have a second to dial;
|
||||||
|
# 127.0.0.1 (the decoy) is the one the old bypass reached.
|
||||||
|
out="$tmpdir/crawl"
|
||||||
|
HTTRACK_DEBUG_RESOLVE="decoyhost:127.0.0.2,127.0.0.1" \
|
||||||
|
httrack "http://decoyhost:$port/simple/basic.html" -O "$out" \
|
||||||
|
-P "127.0.0.1:$deadproxy" -c1 --robots=0 --timeout=5 --retries=0 --quiet -Z \
|
||||||
|
>"$tmpdir/log" 2>&1
|
||||||
|
|
||||||
|
log="$out/hts-log.txt"
|
||||||
|
|
||||||
|
# the origin must have seen no connection (no leak/bypass)
|
||||||
|
if grep -qE '"(GET|POST|HEAD|CONNECT) ' "$tmpdir/srv.err"; then
|
||||||
|
echo "FAIL: origin was contacted directly, bypassing the proxy"
|
||||||
|
cat "$tmpdir/srv.err"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# the crawl must fail at the proxy (proves it really tried the proxy, not a no-op)
|
||||||
|
grep -q 'Connect Error' "$log" || {
|
||||||
|
echo "FAIL: expected a proxy connect error"
|
||||||
|
cat "$log"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# and it must not have tried an origin-address fallback under the proxy
|
||||||
|
if grep -q "trying next address" "$log"; then
|
||||||
|
echo "FAIL: fell back to an origin address under a proxy"
|
||||||
|
cat "$log"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "OK: a dead proxy fails cleanly without dialing the origin"
|
||||||
@@ -131,6 +131,8 @@ TESTS = \
|
|||||||
51_local-update-codec.test \
|
51_local-update-codec.test \
|
||||||
52_local-socks5.test \
|
52_local-socks5.test \
|
||||||
53_local-proxytrack-cache-corrupt.test \
|
53_local-proxytrack-cache-corrupt.test \
|
||||||
54_local-update-truncate-purge.test
|
54_local-update-truncate-purge.test \
|
||||||
|
55_local-chunked.test \
|
||||||
|
56_local-proxy-noleak.test
|
||||||
|
|
||||||
CLEANFILES = check-network_sh.cache
|
CLEANFILES = check-network_sh.cache
|
||||||
|
|||||||
@@ -1215,6 +1215,28 @@ class Handler(SimpleHTTPRequestHandler):
|
|||||||
if self.command != "HEAD":
|
if self.command != "HEAD":
|
||||||
self.wfile.write(body)
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def route_chunked_index(self):
|
||||||
|
self.send_html('\t<a href="page.html">chunked</a>\n')
|
||||||
|
|
||||||
|
def route_chunked_page(self):
|
||||||
|
# Transfer-Encoding: chunked over many small chunks: drives the engine's
|
||||||
|
# chunk automaton (htsback.c). The mirrored file must equal the joined
|
||||||
|
# chunk bodies, so the 2GB in-RAM cap doesn't fire on ordinary traffic.
|
||||||
|
blob = big_html("chunked", "<p>" + "chunk-body " * 300 + "</p>")
|
||||||
|
self.protocol_version = "HTTP/1.1"
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
self.send_header("Transfer-Encoding", "chunked")
|
||||||
|
self.send_header("Connection", "close")
|
||||||
|
self.end_headers()
|
||||||
|
if self.command == "HEAD":
|
||||||
|
return
|
||||||
|
step = 64
|
||||||
|
for off in range(0, len(blob), step):
|
||||||
|
piece = blob[off : off + step]
|
||||||
|
self.wfile.write(b"%X\r\n" % len(piece) + piece + b"\r\n")
|
||||||
|
self.wfile.write(b"0\r\n\r\n")
|
||||||
|
|
||||||
# Content-Disposition naming: the attachment filename replaces the
|
# Content-Disposition naming: the attachment filename replaces the
|
||||||
# URL-derived name; path components in it are stripped (RFC 2616).
|
# URL-derived name; path components in it are stripped (RFC 2616).
|
||||||
CDISPO_NAMES = {
|
CDISPO_NAMES = {
|
||||||
@@ -1554,6 +1576,8 @@ class Handler(SimpleHTTPRequestHandler):
|
|||||||
"/crange206mem/blob.bin": route_crange206mem,
|
"/crange206mem/blob.bin": route_crange206mem,
|
||||||
"/size/index.html": route_size_index,
|
"/size/index.html": route_size_index,
|
||||||
"/size/oversize.bin": route_size_oversize,
|
"/size/oversize.bin": route_size_oversize,
|
||||||
|
"/chunked/index.html": route_chunked_index,
|
||||||
|
"/chunked/page.html": route_chunked_page,
|
||||||
"/errpage/index.html": route_errpage_index,
|
"/errpage/index.html": route_errpage_index,
|
||||||
"/errpage/good.html": route_errpage_good,
|
"/errpage/good.html": route_errpage_good,
|
||||||
"/errpage/missing.html": route_errpage_missing,
|
"/errpage/missing.html": route_errpage_missing,
|
||||||
|
|||||||
Reference in New Issue
Block a user