mirror of
https://github.com/xroche/httrack.git
synced 2026-07-27 02:52:42 +03:00
Compare commits
1 Commits
feat/singl
...
fix/lienre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8b950bfac |
6
.github/workflows/windows-build.yml
vendored
6
.github/workflows/windows-build.yml
vendored
@@ -226,10 +226,8 @@ jobs:
|
||||
# Every gate here exits 77, so an all-skipped suite would report green having
|
||||
# tested nothing: pin the skips, and floor the passes in case the glob empties.
|
||||
# footer-overflow skips on Windows (needs a path past MAX_PATH); crange pending #581;
|
||||
# webdav-mime needs a reapable background listener, which MSYS cannot give it;
|
||||
# badmtime needs a filesystem that stores an mtime past gmtime's range;
|
||||
# single-file ends on a GUI half needing htsserver, which this job does not build.
|
||||
expected_skips=" 01_engine-footer-overflow.test 48_local-crange-memresume.test 71_local-crange-repaircache.test 79_local-proxytrack-webdav-mime.test 88_local-proxytrack-badmtime.test 93_local-single-file.test"
|
||||
# webdav-mime needs a reapable background listener, which MSYS cannot give it.
|
||||
expected_skips=" 01_engine-footer-overflow.test 48_local-crange-memresume.test 71_local-crange-repaircache.test 79_local-proxytrack-webdav-mime.test"
|
||||
[ "$pass" -ge 90 ] || { echo "::error::only $pass tests passed ($skip skipped)"; exit 1; }
|
||||
[ "$skipped" = "$expected_skips" ] || { echo "::error::unexpected skips:$skipped"; exit 1; }
|
||||
[ "$fail" -eq 0 ] || { echo "::error::failing:$failed"; exit 1; }
|
||||
|
||||
27
AGENTS.md
27
AGENTS.md
@@ -26,12 +26,6 @@ the operational checklist: toolchain, invariants, and how to ship a change.
|
||||
check`, or `PATH="<bld>/src:$PATH"` for a manual run.
|
||||
- Give new `.test` scripts `set -e`: the older ones predate the rule, so several
|
||||
`local-crawl.sh` calls with no `set -e` report PASS on any non-last failure.
|
||||
- Never assert with `cmd | grep -q MARKER && fail`. Under `pipefail` the
|
||||
pipeline is non-zero both when `cmd` fails and when `grep -q` matches early
|
||||
and SIGPIPEs it, so the `&&` never fires and a probe that proved nothing reads
|
||||
as "marker absent". Capture the reply, assert the status line it must carry
|
||||
(an empty, truncated or redirected one is marker-free too), then match with a
|
||||
here-string.
|
||||
|
||||
## Hard invariants
|
||||
- **Generated autotools files are NOT in git.** `configure`, every
|
||||
@@ -54,16 +48,6 @@ the operational checklist: toolchain, invariants, and how to ship a change.
|
||||
- Bounds-check every copy. Overflow-safe form: put the untrusted value alone,
|
||||
`untrusted < limit - controlled` — never `controlled + untrusted < limit`,
|
||||
which can wrap and pass.
|
||||
- **Abort or clip is a decision, not a default.** The `*_safe_` helpers
|
||||
(`strcpybuff`, `strlcpybuff`, `strcatbuff`) **abort** on overflow. Right for
|
||||
our own data, wrong for anything read back from a cache, a header or the
|
||||
wire, where it trades a memory smash for a crash on malformed input. Clip
|
||||
with `dst[0] = '\0'; strlncatbuff(dst, src, size, size - 1)`.
|
||||
- **A warning class is not the unsafe set.** `-Wformat-truncation` fires only on
|
||||
a *bounded* `snprintf` whose return is discarded, so an unbounded `sprintf`
|
||||
into the same buffer never appears on it. Before scoping a hardening pass off
|
||||
compiler output, grep the unguarded forms yourself (`\bsprintf\s*\(`,
|
||||
`\bstrcpy\s*\(`, `\bstrcat\s*\(`).
|
||||
|
||||
## C conventions
|
||||
- **Use the `*t` allocator wrappers, never raw libc** (`htssafe.h`):
|
||||
@@ -100,17 +84,6 @@ Before pushing, and when reviewing others, don't skim for bugs:
|
||||
layout/ABI, cache/wire format, or a security path? A static or unit check
|
||||
isn't enough; exercise the wrong behavior at runtime. Claude Code:
|
||||
`/review-recipe`.
|
||||
- **Poison a canary, never compare it against zero.** Checking that a
|
||||
neighbouring field is still `'\0'` cannot see the stray NUL an off-by-one
|
||||
terminator writes — the exact bug the canary is there for. Fill it with a
|
||||
non-zero byte, and prove it by killing both the stray-`'X'` and the
|
||||
stray-NUL mutant. Neither ASan nor `_FORTIFY_SOURCE` sees an overflow that
|
||||
lands inside the same struct.
|
||||
- **Overshoot every destination, not one.** A bounds test that oversizes a
|
||||
single field cannot tell a per-field bound from a one-size-fits-all one, nor
|
||||
from a fix that bounds that field and leaves its neighbours raw. Exercise
|
||||
each destination the path touches, spanning at least two capacities, and
|
||||
check what the code actually emits before writing the expected values.
|
||||
|
||||
## Commits
|
||||
- **Sign-off is mandatory.** Every commit carries a `Signed-off-by` trailer:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
if FUZZERS
|
||||
noinst_PROGRAMS = fuzz-charset fuzz-meta fuzz-idna fuzz-entities \
|
||||
fuzz-unescape fuzz-filters fuzz-url fuzz-header fuzz-cachendx \
|
||||
fuzz-htsparse fuzz-singlefile
|
||||
fuzz-htsparse
|
||||
endif
|
||||
|
||||
AM_CPPFLAGS = \
|
||||
@@ -27,7 +27,6 @@ fuzz_url_SOURCES = fuzz-url.c fuzz.h
|
||||
fuzz_header_SOURCES = fuzz-header.c fuzz.h
|
||||
fuzz_cachendx_SOURCES = fuzz-cachendx.c fuzz.h
|
||||
fuzz_htsparse_SOURCES = fuzz-htsparse.c fuzz.h
|
||||
fuzz_singlefile_SOURCES = fuzz-singlefile.c fuzz.h
|
||||
|
||||
# List corpus files explicitly: automake does not expand EXTRA_DIST globs.
|
||||
EXTRA_DIST = README.md run-fuzzers.sh \
|
||||
@@ -48,8 +47,4 @@ EXTRA_DIST = README.md run-fuzzers.sh \
|
||||
corpus/cachendx/regress-overadvance.bin \
|
||||
corpus/cachendx/regress-truncated-entry.bin \
|
||||
corpus/htsparse/basic.html corpus/htsparse/script-inscript.html \
|
||||
corpus/htsparse/meta-usemap.html corpus/htsparse/malformed.html \
|
||||
corpus/singlefile/img-src.html corpus/singlefile/link-rel.html \
|
||||
corpus/singlefile/style-block.html corpus/singlefile/style-attr.html \
|
||||
corpus/singlefile/srcset.html corpus/singlefile/rawtext.html \
|
||||
corpus/singlefile/malformed.html corpus/singlefile/many-attrs.html
|
||||
corpus/htsparse/meta-usemap.html corpus/htsparse/malformed.html
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<img src="a.png">
|
||||
<img src=big.png alt=over-cap>
|
||||
<img src="../escape.png"><img src="/abs.png"><img src="data:,x">
|
||||
@@ -1,4 +0,0 @@
|
||||
<link rel="stylesheet" href="s.css">
|
||||
<link rel=icon href=a.png>
|
||||
<link rel="next" href="p2.html">
|
||||
<link rel="preload" href="j.js">
|
||||
@@ -1,5 +0,0 @@
|
||||
<img src="unterminated.png
|
||||
<div style="background:url(a.png">
|
||||
<style>@import url(
|
||||
<!-- unterminated comment
|
||||
<a href=
|
||||
@@ -1 +0,0 @@
|
||||
<img src="a.png" a0="v" a1="v" a2="v" a3="v" a4="v" a5="v" a6="v" a7="v" a8="v" a9="v" a10="v" a11="v" a12="v" a13="v" a14="v" a15="v" a16="v" a17="v" a18="v" a19="v" a20="v" a21="v" a22="v" a23="v" a24="v" a25="v" a26="v" a27="v" a28="v" a29="v" a30="v" a31="v" a32="v" a33="v" a34="v" a35="v" a36="v" a37="v" a38="v" a39="v" a40="v" a41="v" a42="v" a43="v" a44="v" a45="v" a46="v" a47="v" a48="v" a49="v" a50="v" a51="v" a52="v" a53="v" a54="v" a55="v" a56="v" a57="v" a58="v" a59="v" a60="v" a61="v" a62="v" a63="v" a64="v" a65="v" a66="v" a67="v" a68="v" a69="v">
|
||||
@@ -1,4 +0,0 @@
|
||||
<script>var s="</scripting>"; if(a</b) x=1;</script>
|
||||
<script src="j.js"></script>
|
||||
<textarea></textareas></textarea>
|
||||
<title></titles></title>
|
||||
@@ -1,2 +0,0 @@
|
||||
<img srcset="a.png 1x, big.png 2x, a.png 100w">
|
||||
<source srcset="a.png,, a.png 2x," src="a.png">
|
||||
@@ -1,2 +0,0 @@
|
||||
<div style="background:url(a.png);list-style:url('a.png')"></div>
|
||||
<p style='background:url("a.png")'>x</p>
|
||||
@@ -1,5 +0,0 @@
|
||||
<style>@import "s.css";
|
||||
@import url(sub/b.css);
|
||||
div{background:url(a.png)}
|
||||
/* url(a.png) */ p:after{content:"url(a.png)"}
|
||||
</style>
|
||||
@@ -1,131 +0,0 @@
|
||||
/* ------------------------------------------------------------ */
|
||||
/*
|
||||
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
|
||||
*/
|
||||
|
||||
/* Fuzz the --single-file rewriter (htssinglefile.c): hostile HTML walked
|
||||
through the tag, CSS url()/@import and srcset parsers, then re-serialized.
|
||||
The resolver is aimed at a private temp tree, so the inlining half (MIME
|
||||
guess, base64, nested stylesheet) is reached and nothing else on disk is. */
|
||||
#include "fuzz.h"
|
||||
|
||||
#include "httrack-library.h"
|
||||
#include "htssinglefile.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
/* Between a.png and big.png, so one input reaches both the inline path and the
|
||||
over-cap fallback. */
|
||||
#define FUZZ_SF_CAP 64
|
||||
|
||||
static char sf_root[512];
|
||||
static char sf_page[600];
|
||||
|
||||
/* The asset tree, in removal order: the subdirectory comes after its file. */
|
||||
static const char *const sf_files[] = {"a.png", "big.png", "j.js", "s.css",
|
||||
"sub/b.css", "sub", NULL};
|
||||
|
||||
static void sf_cleanup(void) {
|
||||
char path[700];
|
||||
int i;
|
||||
|
||||
for (i = 0; sf_files[i] != NULL; i++) {
|
||||
snprintf(path, sizeof(path), "%s/%s", sf_root, sf_files[i]);
|
||||
(void) remove(path);
|
||||
}
|
||||
(void) remove(sf_root);
|
||||
}
|
||||
|
||||
/* A missing asset would silently reduce the target to its parser half. */
|
||||
static void sf_write(const char *name, const char *data, size_t len) {
|
||||
char path[700];
|
||||
FILE *fp;
|
||||
|
||||
snprintf(path, sizeof(path), "%s/%s", sf_root, name);
|
||||
fp = fopen(path, "wb");
|
||||
if (fp == NULL || fwrite(data, 1, len, fp) != len)
|
||||
abort();
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
static void sf_text(const char *name, const char *data) {
|
||||
sf_write(name, data, strlen(data));
|
||||
}
|
||||
|
||||
static void sf_init(void) {
|
||||
static const char png[] = "\x89PNG\r\n\x1a\n";
|
||||
static const char big[4096] = "\x89PNG";
|
||||
const char *tmp = getenv("TMPDIR");
|
||||
char path[700];
|
||||
|
||||
hts_init();
|
||||
snprintf(sf_root, sizeof(sf_root), "%s/httrack-fuzz-sf-XXXXXX",
|
||||
tmp != NULL && tmp[0] != '\0' ? tmp : "/tmp");
|
||||
if (mkdtemp(sf_root) == NULL)
|
||||
abort();
|
||||
atexit(sf_cleanup);
|
||||
snprintf(sf_page, sizeof(sf_page), "%s/page.html", sf_root);
|
||||
snprintf(path, sizeof(path), "%s/sub", sf_root);
|
||||
if (mkdir(path, 0700) != 0)
|
||||
abort();
|
||||
sf_write("a.png", png, sizeof(png) - 1);
|
||||
sf_write("big.png", big, sizeof(big));
|
||||
sf_text("j.js", "var x=1;\n");
|
||||
/* @import plus a url(), so an inlined stylesheet recurses and its own
|
||||
relative reference is rebased. */
|
||||
sf_text("s.css", "@import url(sub/b.css);\ndiv{background:url(a.png)}\n");
|
||||
sf_text("sub/b.css", "p{background:url(../a.png)}\n");
|
||||
}
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
static int inited = 0;
|
||||
|
||||
String out = STRING_EMPTY;
|
||||
httrackp *opt;
|
||||
/* Exact-length, unterminated: the rewriter is span-based, so ASan bounds a
|
||||
read past html_len instead of it landing on a terminator. */
|
||||
char *html = malloct(size != 0 ? size : 1);
|
||||
|
||||
if (!inited) {
|
||||
sf_init();
|
||||
inited = 1;
|
||||
}
|
||||
memcpy(html, data, size);
|
||||
|
||||
opt = hts_create_opt();
|
||||
opt->log = opt->errlog = NULL;
|
||||
opt->single_file_max_size = FUZZ_SF_CAP;
|
||||
|
||||
StringClear(out);
|
||||
(void) singlefile_rewrite_html(opt, sf_root, sf_page, html, size,
|
||||
SINGLEFILE_MAX_PAGE_SIZE, &out);
|
||||
|
||||
StringFree(out);
|
||||
freet(html);
|
||||
hts_free_opt(opt);
|
||||
return 0;
|
||||
}
|
||||
@@ -300,7 +300,6 @@ ones it kept so the local copy browses offline. These options tune both halves.<
|
||||
<tr><td><tt>--preserve (-%p), --disable-passwords (-%x)</tt></td><td>Leave HTML untouched (no rewriting), and strip passwords out of saved links.</td></tr>
|
||||
<tr><td><tt>--extended-parsing (-%P), --parse-java (-j)</tt></td><td>Aggressive link discovery, and how much script content is parsed for links.</td></tr>
|
||||
<tr><td><tt>--mime-html (-%M)</tt></td><td>Save the whole mirror as a single MIME-encapsulated <tt>.mht</tt> archive (<tt>index.mht</tt>).</td></tr>
|
||||
<tr><td><tt>--single-file (-%Z), --single-file-max-size N</tt></td><td>Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded as <tt>data:</tt> URIs. Assets over the cap (10 MB by default) keep their link, as do audio, video, and the links from one page to another. A sibling of <tt>-%M</tt>, not a replacement: see the recipe below for which to pick.</td></tr>
|
||||
<tr><td><tt>--index (-I), --build-top-index (-%i), --search-index (-%I)</tt></td><td>Build a per-mirror index, a top index across projects, and a searchable keyword index.</td></tr>
|
||||
</table>
|
||||
|
||||
@@ -483,25 +482,6 @@ add <tt>--warc-cdx</tt> for a sorted CDXJ index, or <tt>--wacz</tt> to bundle th
|
||||
archive, index and pages into one WACZ for replay tools such as
|
||||
replayweb.page.</small></p>
|
||||
|
||||
<h4>Pages you can hand to someone as one file</h4>
|
||||
<p><tt>httrack https://example.com/ --single-file --path mydir</tt><br>
|
||||
<small>Rewrites each saved page after the crawl so its stylesheets, scripts,
|
||||
images and fonts are embedded as <tt>data:</tt> URIs. The mirror stays a normal
|
||||
browsable tree, with links between pages relative and the assets still on disk,
|
||||
but any single <tt>.html</tt> file can now be mailed or archived on its own.
|
||||
Raise or lower the 10 MB per-asset limit with
|
||||
<tt>--single-file-max-size N</tt>; anything over it, plus audio and video, keeps
|
||||
its link. One caveat if you raise it: an inlined stylesheet becomes a
|
||||
<tt>data:</tt> URL, whose path is opaque, so an asset it referenced that stayed
|
||||
over the cap no longer resolves from inside it. Raising the cap past that asset
|
||||
embeds it too and the question goes away.<br>
|
||||
Reach for this when the file has to open for someone you cannot make assumptions
|
||||
about: it is plain HTML and needs no add-on. Reach for <tt>--mime-html</tt>
|
||||
instead when a Chromium-family browser is a given and the mirror is large: MIME
|
||||
carries text parts without the base64 tax, keeps each resource's original URL,
|
||||
and stores a shared asset once rather than re-embedding it in every page that
|
||||
uses it.</small></p>
|
||||
|
||||
<h4>HTTrack as a fetch tool</h4>
|
||||
<p><tt>httrack --get https://host/file.bin --path tmp</tt><br>
|
||||
<small><tt>--get</tt> fetches one file with cache, index, depth, cookies and robots
|
||||
|
||||
@@ -90,10 +90,9 @@ offline browser : copy websites to a local directory</p>
|
||||
] [ <b>-NN, --structure[=N]</b> ] [ <b>-%N,
|
||||
--delayed-type-check</b> ] [ <b>-%D,
|
||||
--cached-delayed-type-check</b> ] [ <b>-%M, --mime-html</b>
|
||||
] [ <b>-%Z, --single-file</b> ] [ <b>-LN,
|
||||
--long-names[=N]</b> ] [ <b>-KN, --keep-links[=N]</b> ] [
|
||||
<b>-x, --replace-external</b> ] [ <b>-%x,
|
||||
--disable-passwords</b> ] [ <b>-%q,
|
||||
] [ <b>-LN, --long-names[=N]</b> ] [ <b>-KN,
|
||||
--keep-links[=N]</b> ] [ <b>-x, --replace-external</b> ] [
|
||||
<b>-%x, --disable-passwords</b> ] [ <b>-%q,
|
||||
--include-query-string</b> ] [ <b>-%g, --strip-query</b> ] [
|
||||
<b>-o, --generate-errors</b> ] [ <b>-X, --purge-old[=N]</b>
|
||||
] [ <b>-%p, --preserve</b> ] [ <b>-%T, --utf8-conversion</b>
|
||||
@@ -649,24 +648,6 @@ don’t wait) (--cached-delayed-type-check)</p></td></tr>
|
||||
<td width="4%">
|
||||
|
||||
|
||||
<p>-%Z</p></td>
|
||||
<td width="5%"></td>
|
||||
<td width="82%">
|
||||
|
||||
|
||||
<p>after the mirror, rewrite each saved page with its
|
||||
stylesheets, scripts, images and fonts inlined as data:
|
||||
URIs, so any page opens by double-click anywhere (links
|
||||
between pages stay relative; audio and video stay links);
|
||||
--single-file-max-size N caps each asset (default 10485760
|
||||
bytes). %M is the better container where a Chromium-family
|
||||
browser is a given: one archive, no base64 tax on text, a
|
||||
shared asset stored once (--single-file)</p></td></tr>
|
||||
<tr valign="top" align="left">
|
||||
<td width="9%"></td>
|
||||
<td width="4%">
|
||||
|
||||
|
||||
<p>-%t</p></td>
|
||||
<td width="5%"></td>
|
||||
<td width="82%">
|
||||
|
||||
@@ -98,9 +98,6 @@ ${do:end-if}
|
||||
<input type="hidden" name="redirect" value="">
|
||||
<input type="hidden" name="closeme" value="">
|
||||
|
||||
<!-- clear if not checked -->
|
||||
<input type="hidden" name="ftpprox" value="">
|
||||
|
||||
${LANG_PROXYTYPE}
|
||||
<select name="proxytype"
|
||||
title='${html:LANG_PROXYTYPETIP}' onMouseOver="info('${html:LANG_PROXYTYPETIP}'); return true" onMouseOut="info(' '); return true"
|
||||
|
||||
@@ -98,14 +98,6 @@ ${do:end-if}
|
||||
<input type="hidden" name="redirect" value="">
|
||||
<input type="hidden" name="closeme" value="">
|
||||
|
||||
<!-- clear if not checked -->
|
||||
<input type="hidden" name="errpage" value="">
|
||||
<input type="hidden" name="external" value="">
|
||||
<input type="hidden" name="hidepwd" value="">
|
||||
<input type="hidden" name="hidequery" value="">
|
||||
<input type="hidden" name="nopurge" value="">
|
||||
<input type="hidden" name="singlefile" value="">
|
||||
|
||||
${LANG_I33}
|
||||
<br>
|
||||
<select name="build"
|
||||
@@ -144,13 +136,6 @@ ${listid:build:LISTDEF_3}
|
||||
<tr><td><input type="checkbox" name="nopurge" ${checked:nopurge}
|
||||
title='${html:LANG_I1a}' onMouseOver="info('${html:LANG_I1a}'); return true" onMouseOut="info(' '); return true"
|
||||
> ${LANG_I57}</td></tr>
|
||||
<tr><td><input type="checkbox" name="singlefile" ${checked:singlefile}
|
||||
title='${html:LANG_SINGLEFILETIP}' onMouseOver="info('${html:LANG_SINGLEFILETIP}'); return true" onMouseOut="info(' '); return true"
|
||||
> ${LANG_SINGLEFILE}</td></tr>
|
||||
<tr><td>${LANG_SINGLEFILEMAX}
|
||||
<input name="singlefilemax" value="${singlefilemax}" size="12"
|
||||
title='${html:LANG_SINGLEFILEMAXTIP}' onMouseOver="info('${html:LANG_SINGLEFILEMAXTIP}'); return true" onMouseOut="info(' '); return true"
|
||||
></td></tr>
|
||||
</table>
|
||||
|
||||
<tr><td>
|
||||
|
||||
@@ -98,9 +98,6 @@ ${do:end-if}
|
||||
<input type="hidden" name="redirect" value="">
|
||||
<input type="hidden" name="closeme" value="">
|
||||
|
||||
<!-- clear if not checked -->
|
||||
<input type="hidden" name="windebug" value="">
|
||||
|
||||
${LANG_I40c}
|
||||
<br>
|
||||
|
||||
|
||||
@@ -98,11 +98,6 @@ ${do:end-if}
|
||||
<input type="hidden" name="redirect" value="">
|
||||
<input type="hidden" name="closeme" value="">
|
||||
|
||||
<!-- clear if not checked -->
|
||||
<input type="hidden" name="ka" value="">
|
||||
<input type="hidden" name="remt" value="">
|
||||
<input type="hidden" name="rems" value="">
|
||||
|
||||
<table border="0" width="100%" cellspacing="0">
|
||||
|
||||
<tr><td>
|
||||
|
||||
@@ -98,17 +98,6 @@ ${do:end-if}
|
||||
<input type="hidden" name="redirect" value="">
|
||||
<input type="hidden" name="closeme" value="">
|
||||
|
||||
<!-- clear if not checked -->
|
||||
<input type="hidden" name="cookies" value="">
|
||||
<input type="hidden" name="parsejava" value="">
|
||||
<input type="hidden" name="updhack" value="">
|
||||
<input type="hidden" name="urlhack" value="">
|
||||
<input type="hidden" name="keepwww" value="">
|
||||
<input type="hidden" name="keepslashes" value="">
|
||||
<input type="hidden" name="keepqueryorder" value="">
|
||||
<input type="hidden" name="toler" value="">
|
||||
<input type="hidden" name="http10" value="">
|
||||
|
||||
<input type="checkbox" name="cookies" ${checked:cookies}
|
||||
title='${html:LANG_I1b}' onMouseOver="info('${html:LANG_I1b}'); return true" onMouseOut="info(' '); return true"
|
||||
> ${LANG_I58}
|
||||
|
||||
@@ -98,13 +98,6 @@ ${do:end-if}
|
||||
<input type="hidden" name="redirect" value="">
|
||||
<input type="hidden" name="closeme" value="">
|
||||
|
||||
<!-- clear if not checked -->
|
||||
<input type="hidden" name="warc" value="">
|
||||
<input type="hidden" name="norecatch" value="">
|
||||
<input type="hidden" name="logf" value="">
|
||||
<input type="hidden" name="index" value="">
|
||||
<input type="hidden" name="index2" value="">
|
||||
|
||||
<input type="checkbox" name="cache2" ${checked:cache2}
|
||||
title='${html:LANG_I1e}' onMouseOver="info('${html:LANG_I1e}'); return true" onMouseOut="info(' '); return true"
|
||||
> ${LANG_I61}
|
||||
|
||||
@@ -143,8 +143,6 @@ ${do:copy:StripQuery:stripquery}
|
||||
${do:copy:StoreAllInCache:cache2}
|
||||
${do:copy:Warc:warc}
|
||||
${do:copy:WarcFile:warcfile}
|
||||
${do:copy:SingleFile:singlefile}
|
||||
${do:copy:SingleFileMaxSize:singlefilemax}
|
||||
${do:copy:LogType:logtype}
|
||||
${do:copy:UseHTTPProxyForFTP:ftpprox}
|
||||
${do:copy:ProxyType:proxytype}
|
||||
|
||||
@@ -114,7 +114,7 @@ ${do:end-if}
|
||||
|
||||
${/* Real commands and ini file generated below */}
|
||||
|
||||
<!-- engine commandline; ztest so a cleared default-on option still emits its disabling flag -->
|
||||
<!-- engine commandline -->
|
||||
${do:output-mode:html}
|
||||
<textarea name="command" cols="50" rows="4" style="visibility:hidden">
|
||||
httrack \
|
||||
@@ -175,8 +175,8 @@ ${/* -m<n> resets the html limit, so the bare form must precede the -m,<n> one *
|
||||
\
|
||||
${unquoted:url2}
|
||||
\
|
||||
${ztest:cookies:--cookies=0:}
|
||||
${ztest:parsejava:--parse-java=0:}
|
||||
${test:cookies:--cookies=0:}
|
||||
${test:parsejava:--parse-java=0:}
|
||||
${test:updhack:--updatehack}
|
||||
${test:urlhack:--urlhack=0:--urlhack}
|
||||
${test:keepwww:--keep-www-prefix}
|
||||
@@ -190,8 +190,6 @@ ${/* -m<n> resets the html limit, so the bare form must precede the -m,<n> one *
|
||||
${test:cache2:--store-all-in-cache}
|
||||
${test:warc:--warc}
|
||||
${test:warcfile:--warc-file "}${arg:warcfile}${test:warcfile:"}
|
||||
${test:singlefile:--single-file}
|
||||
${test:singlefilemax:--single-file-max-size=}${unquoted:singlefilemax}
|
||||
${test:norecatch:--do-not-recatch}
|
||||
${test:logf:--single-log}
|
||||
${test:logtype:::--extra-log:--debug-log}
|
||||
@@ -244,8 +242,6 @@ StripQuery=${stripquery}
|
||||
StoreAllInCache=${ztest:cache2:0:1}
|
||||
Warc=${ztest:warc:0:1}
|
||||
WarcFile=${warcfile}
|
||||
SingleFile=${ztest:singlefile:0:1}
|
||||
SingleFileMaxSize=${singlefilemax}
|
||||
LogType=${logtype}
|
||||
UseHTTPProxyForFTP=${ztest:ftpprox:0:1}
|
||||
ProxyType=${proxytype}
|
||||
|
||||
8
lang.def
8
lang.def
@@ -1042,11 +1042,3 @@ LANG_WARCFILE
|
||||
WARC archive name:
|
||||
LANG_WARCFILETIP
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
LANG_SINGLEFILE
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
LANG_SINGLEFILETIP
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
LANG_SINGLEFILEMAX
|
||||
Largest inlined asset (bytes):
|
||||
LANG_SINGLEFILEMAXTIP
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
Èìå íà WARC àðõèâà:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Íåçàäúëæèòåëíî áàçîâî èìå çà WARC àðõèâà; îñòàâåòå ïðàçíî çà àâòîìàòè÷íî èìåíóâàíå â èçõîäíàòà äèðåêòîðèÿ.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Âãðàæäàíå íà ðåñóðñèòå êàòî data: URI (ñàìîñòîÿòåëíè ñòðàíèöè)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Ñëåä çàâúðøâàíå íà îãëåäàëîòî âñÿêà çàïàçåíà ñòðàíèöà ñå ïðåçàïèñâà ñ âãðàäåíè ñòèëîâå, ñêðèïòîâå, èçîáðàæåíèÿ è øðèôòîâå, òàêà ÷å ñòðàíèöàòà äà ìîæå äà ñå îòâîðè è ñàìîñòîÿòåëíî.
|
||||
Largest inlined asset (bytes):
|
||||
Íàé-ãîëÿì âãðàäåí ðåñóðñ (áàéòîâå):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Ðåñóðñ íàä òîçè ðàçìåð çàïàçâà îáèêíîâåíà âðúçêà; îñòàâåòå ïðàçíî çà ñòîéíîñòòà ïî ïîäðàçáèðàíå îò 10485760 áàéòà.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
Nombre del archivo WARC:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Nombre base opcional para el archivo WARC; déjelo en blanco para nombrarlo automáticamente en el directorio de salida.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Incrustar los recursos como URI data: (páginas autónomas)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Una vez terminada la copia, reescribir cada página guardada con sus hojas de estilo, scripts, imágenes y fuentes incrustadas, de modo que una página también pueda abrirse por sí sola.
|
||||
Largest inlined asset (bytes):
|
||||
Tamaño máximo del recurso incrustado (bytes):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Un recurso mayor que este tamaño conserva un enlace normal; déjelo vacío para el valor predeterminado de 10485760 bytes.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
Název archivu WARC:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Volitelný základní název archivu WARC; ponechte prázdné pro automatické pojmenování ve výstupním adresáøi.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Vložit zdroje jako URI data: (samostatné stránky)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Po dokonèení zrcadlení pøepsat každou uloženou stránku s vloženými styly, skripty, obrázky a písmy, aby ji bylo možné otevøít i samostatnì.
|
||||
Largest inlined asset (bytes):
|
||||
Nejvìtší vložený zdroj (bajty):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Zdroj vìtší než tato velikost si ponechá bìžný odkaz; ponechte prázdné pro výchozí hodnotu 10485760 bajtù.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
WARC 封存檔名稱:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
WARC 封存檔的選用基本名稱;留空則於輸出目錄中自動命名。
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
將資源內嵌為 data: URI(獨立網頁)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
鏡射完成後,重寫每個已儲存的網頁,將樣式表、指令碼、圖片與字型內嵌其中,讓網頁也能單獨開啟。
|
||||
Largest inlined asset (bytes):
|
||||
內嵌資源大小上限(位元組):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
超過此大小的資源會保留一般連結;留空則使用預設的 10485760 位元組。
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
WARC 归档名称:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
WARC 归档的可选基本名称;留空则在输出目录中自动命名。
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
将资源内嵌为 data: URI(独立网页)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
镜像完成后,重写每个已保存的网页,将样式表、脚本、图片和字体内嵌其中,使网页也能单独打开。
|
||||
Largest inlined asset (bytes):
|
||||
内嵌资源大小上限(字节):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
超过此大小的资源会保留普通链接;留空则使用默认的 10485760 字节。
|
||||
|
||||
@@ -966,11 +966,3 @@ WARC archive name:
|
||||
Naziv WARC arhive:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Neobavezni osnovni naziv WARC arhive; ostavite prazno za automatsko imenovanje u izlaznom direktoriju.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Ugradi resurse kao data: URI-je (samostalne stranice)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Nakon dovr¹etka zrcaljenja ponovno zapi¹i svaku spremljenu stranicu s ugraðenim stilskim listovima, skriptama, slikama i fontovima, tako da se stranica mo¾e otvoriti i zasebno.
|
||||
Largest inlined asset (bytes):
|
||||
Najveæi ugraðeni resurs (bajtovi):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Resurs veæi od ove velièine zadr¾ava obiènu poveznicu; ostavite prazno za zadanih 10485760 bajtova.
|
||||
|
||||
@@ -1012,11 +1012,3 @@ WARC archive name:
|
||||
Navn på WARC-arkiv:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Valgfrit basisnavn til WARC-arkivet; lad feltet stå tomt for automatisk navngivning i outputmappen.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Indlejr ressourcer som data:-URI'er (selvstændige sider)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Når spejlingen er færdig, omskrives hver gemt side med dens typografiark, scripts, billeder og skrifttyper indlejret, så en side også kan åbnes alene.
|
||||
Largest inlined asset (bytes):
|
||||
Største indlejrede ressource (byte):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
En ressource over denne størrelse beholder et almindeligt link; lad feltet stå tomt for standardværdien på 10485760 byte.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
Name des WARC-Archivs:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Optionaler Basisname für das WARC-Archiv; leer lassen, um es automatisch im Ausgabeverzeichnis zu benennen.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Ressourcen als data:-URIs einbetten (eigenständige Seiten)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Nach Abschluss der Spiegelung jede gespeicherte Seite mit eingebetteten Stylesheets, Skripten, Bildern und Schriftarten neu schreiben, sodass eine Seite auch für sich allein geöffnet werden kann.
|
||||
Largest inlined asset (bytes):
|
||||
Größte eingebettete Ressource (Bytes):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Eine Ressource über dieser Größe behält einen gewöhnlichen Link; leer lassen für den Standardwert von 10485760 Bytes.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
WARC-arhiivi nimi:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
WARC-arhiivi valikuline põhinimi; jäta tühjaks, et see väljundkataloogis automaatselt nimetada.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Manusta ressursid data: URI-dena (iseseisvad lehed)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Pärast peegelduse lõppu kirjuta iga salvestatud leht ümber, manustades selle laaditabelid, skriptid, pildid ja fondid, nii et lehte saab avada ka eraldi.
|
||||
Largest inlined asset (bytes):
|
||||
Suurim manustatud ressurss (baiti):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Sellest suurem ressurss säilitab tavalise lingi; jäta tühjaks vaikeväärtuse 10485760 baiti jaoks.
|
||||
|
||||
@@ -1012,11 +1012,3 @@ WARC archive name:
|
||||
WARC archive name:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Largest inlined asset (bytes):
|
||||
Largest inlined asset (bytes):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
|
||||
@@ -966,11 +966,3 @@ WARC archive name:
|
||||
WARC-arkiston nimi:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Valinnainen WARC-arkiston perusnimi; jätä tyhjäksi, jotta se nimetään automaattisesti tulostehakemistoon.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Upota resurssit data:-URI-osoitteina (itsenäiset sivut)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Kun peilaus on valmis, kirjoita jokainen tallennettu sivu uudelleen niin, että sen tyylitiedostot, komentosarjat, kuvat ja fontit upotetaan, jolloin sivun voi avata myös yksinään.
|
||||
Largest inlined asset (bytes):
|
||||
Suurin upotettu resurssi (tavua):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Tätä suurempi resurssi säilyttää tavallisen linkin; jätä tyhjäksi, jolloin käytetään oletusarvoa 10485760 tavua.
|
||||
|
||||
@@ -1012,11 +1012,3 @@ WARC archive name:
|
||||
Nom de l'archive WARC :
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Nom de base optionnel pour l'archive WARC ; laissez vide pour le générer automatiquement dans le répertoire de sortie.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Intégrer les ressources en URI data: (pages autonomes)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Une fois le miroir terminé, réécrire chaque page enregistrée avec ses feuilles de style, scripts, images et polices intégrés, afin qu'une page puisse aussi être ouverte seule.
|
||||
Largest inlined asset (bytes):
|
||||
Taille maximale d'une ressource intégrée (octets) :
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Au-delà de cette taille, la ressource reste un lien ordinaire ; laissez vide pour la valeur par défaut de 10485760 octets.
|
||||
|
||||
@@ -966,11 +966,3 @@ WARC archive name:
|
||||
Όνομα αρχείου WARC:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Προαιρετικό βασικό όνομα για το αρχείο WARC. Αφήστε το κενό για αυτόματη ονομασία στον κατάλογο εξόδου.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Ενσωμάτωση των πόρων ως URI data: (αυτόνομες σελίδες)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Μόλις ολοκληρωθεί το αντίγραφο, κάθε αποθηκευμένη σελίδα ξαναγράφεται με ενσωματωμένα τα φύλλα στυλ, τα σενάρια, τις εικόνες και τις γραμματοσειρές της, ώστε η σελίδα να μπορεί να ανοίξει και μόνη της.
|
||||
Largest inlined asset (bytes):
|
||||
Μέγιστος ενσωματωμένος πόρος (byte):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Πόρος μεγαλύτερος από αυτό το μέγεθος διατηρεί κανονικό σύνδεσμο. Αφήστε το κενό για την προεπιλογή των 10485760 byte.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
Nome dell'archivio WARC:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Nome di base facoltativo per l'archivio WARC; lascia vuoto per assegnarlo automaticamente nella directory di output.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Incorpora le risorse come URI data: (pagine autonome)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Al termine della copia, riscrive ogni pagina salvata incorporando fogli di stile, script, immagini e caratteri, in modo che una pagina possa essere aperta anche da sola.
|
||||
Largest inlined asset (bytes):
|
||||
Dimensione massima della risorsa incorporata (byte):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Una risorsa oltre questa dimensione mantiene un collegamento normale; lasciare vuoto per il valore predefinito di 10485760 byte.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
WARC アーカイブ名:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
WARC アーカイブの任意のベース名。空欄にすると出力ディレクトリ内で自動的に名前が付けられます。
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
リソースを data: URI として埋め込む (単独で開けるページ)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
ミラーの完了後、保存された各ページを、スタイルシート、スクリプト、画像、フォントを埋め込んだ形で書き直します。ページ単体でも開けるようになります。
|
||||
Largest inlined asset (bytes):
|
||||
埋め込む最大サイズ (バイト):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
このサイズを超えるリソースは通常のリンクのままになります。空欄にすると既定値の 10485760 バイトになります。
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
¸ÜÕ ÝÐ WARC ÐàåØÒÐâÐ:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
¸×ÑÞàÝÞ ÞáÝÞÒÝÞ ØÜÕ ×Ð WARC ÐàåØÒÐâÐ; ÞáâÐÒÕâÕ ßàÐ×ÝÞ ×Ð ÐÒâÞÜÐâáÚÞ ØÜÕÝãÒÐúÕ ÒÞ Ø×ÛÕ×ÝØÞâ ÔØàÕÚâÞàØãÜ.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
²ÓàÐÔØ ÓØ àÕáãàáØâÕ ÚÐÚÞ data: URI (áÐÜÞáâÞøÝØ áâàÐÝØæØ)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
¿Þ ×ÐÒàèãÒÐúÕ ÝÐ ÞÓÛÕÔÐÛÞâÞ, áÕÚÞøÐ ×ÐçãÒÐÝÐ áâàÐÝØæÐ áÕ ßàÕߨèãÒÐ áÞ ÒÓàÐÔÕÝØ áâØÛáÚØ ÛØáâÞÒØ, áÚàØßâØ, áÛØÚØ Ø äÞÝâÞÒØ, ×Ð ÔÐ ÜÞÖÕ áâàÐÝØæÐâÐ ÔÐ áÕ ÞâÒÞàØ Ø áÐÜÞáâÞøÝÞ.
|
||||
Largest inlined asset (bytes):
|
||||
½ÐøÓÞÛÕÜ ÒÓàÐÔÕÝ àÕáãàá (ÑÐøâØ):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
ÀÕáãàá ßÞÓÞÛÕÜ ÞÔ ÞÒÐÐ ÓÞÛÕÜØÝÐ ×ÐÔàÖãÒÐ ÞÑØçÝÐ ÒàáÚÐ; ÞáâÐÒÕâÕ ßàÐ×ÝÞ ×Ð áâÐÝÔÐàÔÝØâÕ 10485760 ÑÐøâØ.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
WARC archívum neve:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
A WARC archívum opcionális alapneve; hagyja üresen az automatikus elnevezéshez a kimeneti könyvtárban.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Erõforrások beágyazása data: URI-ként (önálló oldalak)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
A tükrözés befejezése után minden mentett oldal újraírása a stíluslapok, parancsfájlok, képek és betûtípusok beágyazásával, így az oldal önmagában is megnyitható.
|
||||
Largest inlined asset (bytes):
|
||||
Legnagyobb beágyazott erõforrás (bájt):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Az ennél nagyobb erõforrás közönséges hivatkozás marad; hagyja üresen a 10485760 bájtos alapértelmezéshez.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
Naam van WARC-archief:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Optionele basisnaam voor het WARC-archief; laat leeg om het automatisch een naam te geven in de uitvoermap.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Bronnen insluiten als data:-URI's (op zichzelf staande pagina's)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Zodra de spiegeling klaar is, elke opgeslagen pagina herschrijven met ingesloten stijlbladen, scripts, afbeeldingen en lettertypen, zodat een pagina ook los geopend kan worden.
|
||||
Largest inlined asset (bytes):
|
||||
Grootste ingesloten bron (bytes):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Een bron boven deze grootte houdt een gewone koppeling; laat leeg voor de standaardwaarde van 10485760 bytes.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
Navn på WARC-arkiv:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Valgfritt basisnavn for WARC-arkivet; la feltet stå tomt for automatisk navngivning i utdatamappen.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Bygg inn ressurser som data:-URI-er (selvstendige sider)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Når speilingen er ferdig, skrives hver lagrede side om med stilark, skript, bilder og skrifter innebygd, slik at en side også kan åpnes alene.
|
||||
Largest inlined asset (bytes):
|
||||
Største innebygde ressurs (byte):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
En ressurs over denne størrelsen beholder en vanlig lenke; la feltet stå tomt for standardverdien på 10485760 byte.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
Nazwa archiwum WARC:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Opcjonalna nazwa bazowa archiwum WARC; pozostaw puste, aby nazwaæ je automatycznie w katalogu wyj¶ciowym.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Osad¼ zasoby jako identyfikatory data: URI (samodzielne strony)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Po zakoñczeniu tworzenia kopii przepisz ka¿d± zapisan± stronê z osadzonymi arkuszami stylów, skryptami, obrazami i czcionkami, aby stronê mo¿na by³o otworzyæ równie¿ samodzielnie.
|
||||
Largest inlined asset (bytes):
|
||||
Najwiêkszy osadzony zasób (bajty):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Zasób wiêkszy ni¿ ten rozmiar zachowuje zwyk³y odno¶nik; pozostaw puste, aby u¿yæ domy¶lnych 10485760 bajtów.
|
||||
|
||||
@@ -1012,11 +1012,3 @@ WARC archive name:
|
||||
Nome do arquivo WARC:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Nome base opcional para o arquivo WARC; deixe em branco para nomeá-lo automaticamente no diretório de saída.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Incorporar os recursos como URIs data: (páginas autônomas)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Ao terminar o espelhamento, reescrever cada página salva com suas folhas de estilo, scripts, imagens e fontes incorporadas, para que a página também possa ser aberta sozinha.
|
||||
Largest inlined asset (bytes):
|
||||
Maior recurso incorporado (bytes):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Um recurso acima desse tamanho mantém um link comum; deixe em branco para o padrão de 10485760 bytes.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
Nome do arquivo WARC:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Nome base opcional para o arquivo WARC; deixe em branco para o nomear automaticamente no diretório de saída.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Incorporar os recursos como URI data: (páginas autónomas)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Depois de terminado o espelho, reescrever cada página guardada com as suas folhas de estilo, scripts, imagens e tipos de letra incorporados, para que a página também possa ser aberta sozinha.
|
||||
Largest inlined asset (bytes):
|
||||
Maior recurso incorporado (bytes):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Um recurso acima deste tamanho mantém uma ligação normal; deixe em branco para o valor predefinido de 10485760 bytes.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
Numele arhivei WARC:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Nume de baza optional pentru arhiva WARC; lasati gol pentru a-l denumi automat in directorul de iesire.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Încorporeaza resursele ca URI data: (pagini de sine statatoare)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Dupa terminarea copiei, fiecare pagina salvata este rescrisa cu foile de stil, scripturile, imaginile si fonturile încorporate, astfel încât pagina sa poata fi deschisa si singura.
|
||||
Largest inlined asset (bytes):
|
||||
Cea mai mare resursa încorporata (octeti):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
O resursa mai mare decât aceasta dimensiune pastreaza o legatura obisnuita; lasati gol pentru valoarea implicita de 10485760 de octeti.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
Èìÿ WARC-àðõèâà:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Íåîáÿçàòåëüíîå áàçîâîå èìÿ WARC-àðõèâà; îñòàâüòå ïóñòûì äëÿ àâòîìàòè÷åñêîãî èìåíîâàíèÿ â âûõîäíîì êàòàëîãå.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Âñòðàèâàòü ðåñóðñû êàê data: URI (ñàìîñòîÿòåëüíûå ñòðàíèöû)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Ïîñëå çàâåðøåíèÿ çåðêàëèðîâàíèÿ êàæäàÿ ñîõðàí¸ííàÿ ñòðàíèöà ïåðåçàïèñûâàåòñÿ ñî âñòðîåííûìè òàáëèöàìè ñòèëåé, ñöåíàðèÿìè, èçîáðàæåíèÿìè è øðèôòàìè, ÷òîáû ñòðàíèöó ìîæíî áûëî îòêðûòü îòäåëüíî.
|
||||
Largest inlined asset (bytes):
|
||||
Íàèáîëüøèé âñòðàèâàåìûé ðåñóðñ (áàéòû):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Ðåñóðñ áîëüøå ýòîãî ðàçìåðà ñîõðàíÿåò îáû÷íóþ ññûëêó; îñòàâüòå ïóñòûì äëÿ çíà÷åíèÿ ïî óìîë÷àíèþ 10485760 áàéò.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
Názov archívu WARC:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Voliteµný základný názov archívu WARC; ponechajte prázdne pre automatické pomenovanie vo výstupnom adresári.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Vlo¾i» zdroje ako URI data: (samostatné stránky)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Po dokonèení zrkadlenia prepísa» ka¾dú ulo¾enú stránku s vlo¾enými ¹týlmi, skriptmi, obrázkami a písmami, aby sa stránka dala otvori» aj samostatne.
|
||||
Largest inlined asset (bytes):
|
||||
Najväè¹í vlo¾ený zdroj (bajty):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Zdroj väè¹í ne¾ táto veµkos» si ponechá be¾ný odkaz; ponechajte prázdne pre predvolených 10485760 bajtov.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
Ime arhiva WARC:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Neobvezno osnovno ime arhiva WARC; pustite prazno za samodejno poimenovanje v izhodni mapi.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Vgradi vire kot data: URI (samostojne strani)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Ko je zrcaljenje koncano, se vsaka shranjena stran prepise z vgrajenimi slogovnimi listi, skripti, slikami in pisavami, tako da jo je mogoce odpreti tudi samostojno.
|
||||
Largest inlined asset (bytes):
|
||||
Najvecji vgrajeni vir (bajti):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Vir, vecji od te velikosti, ohrani obicajno povezavo; pustite prazno za privzetih 10485760 bajtov.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
WARC-arkivets namn:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Valfritt basnamn för WARC-arkivet; lämna tomt för att namnge det automatiskt i utdatakatalogen.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Bädda in resurser som data:-URI:er (fristående sidor)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
När speglingen är klar skrivs varje sparad sida om med sina formatmallar, skript, bilder och teckensnitt inbäddade, så att en sida också kan öppnas för sig.
|
||||
Largest inlined asset (bytes):
|
||||
Största inbäddade resurs (byte):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
En resurs över den här storleken behåller en vanlig länk; lämna tomt för standardvärdet 10485760 byte.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
WARC arþivi adý:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
WARC arþivi için isteðe baðlý temel ad; çýktý dizininde otomatik adlandýrma için boþ býrakýn.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Kaynaklarý data: URI olarak göm (kendi kendine yeten sayfalar)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Yansýlama bittiðinde, kaydedilen her sayfa stil sayfalarý, betikleri, görselleri ve yazý tipleri gömülü olarak yeniden yazýlýr; böylece sayfa tek baþýna da açýlabilir.
|
||||
Largest inlined asset (bytes):
|
||||
En büyük gömülü kaynak (bayt):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Bu boyutun üzerindeki bir kaynak sýradan baðlantýsýný korur; 10485760 baytlýk varsayýlan için boþ býrakýn.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
²ì'ÿ WARC-àðõ³âó:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
Íåîáîâ'ÿçêîâà áàçîâà íàçâà WARC-àðõ³âó; çàëèøòå ïîðîæí³ì äëÿ àâòîìàòè÷íîãî íàéìåíóâàííÿ ó âèõ³äíîìó êàòàëîç³.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Âáóäîâóâàòè ðåñóðñè ÿê data: URI (ñàìîñò³éí³ ñòîð³íêè)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
ϳñëÿ çàâåðøåííÿ äçåðêàëþâàííÿ êîæíà çáåðåæåíà ñòîð³íêà ïåðåçàïèñóºòüñÿ ç âáóäîâàíèìè òàáëèöÿìè ñòèë³â, ñöåíàð³ÿìè, çîáðàæåííÿìè òà øðèôòàìè, ùîá ñòîð³íêó ìîæíà áóëî â³äêðèòè îêðåìî.
|
||||
Largest inlined asset (bytes):
|
||||
Íàéá³ëüøèé âáóäîâàíèé ðåñóðñ (áàéòè):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Ðåñóðñ, á³ëüøèé çà öåé ðîçì³ð, çáåð³ãຠçâè÷àéíå ïîñèëàííÿ; çàëèøòå ïîðîæí³ì äëÿ òèïîâîãî çíà÷åííÿ 10485760 áàéò³â.
|
||||
|
||||
@@ -964,11 +964,3 @@ WARC archive name:
|
||||
WARC arxivi nomi:
|
||||
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
|
||||
WARC arxivi uchun ixtiyoriy asosiy nom; chiqish katalogida avtomatik nomlash uchun bo'sh qoldiring.
|
||||
Inline assets as data: URIs (self-contained pages)
|
||||
Resurslarni data: URI sifatida joylash (mustaqil sahifalar)
|
||||
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
|
||||
Nusxalash tugagach, har bir saqlangan sahifa uslublar jadvallari, skriptlar, rasmlar va shriftlar joylangan holda qayta yoziladi, shunda sahifani alohida ham ochish mumkin.
|
||||
Largest inlined asset (bytes):
|
||||
Eng katta joylangan resurs (bayt):
|
||||
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
|
||||
Bu o’lchamdan katta resurs oddiy havolani saqlab qoladi; standart 10485760 bayt uchun bo’sh qoldiring.
|
||||
|
||||
@@ -71,9 +71,7 @@ static int mysavename(t_hts_callbackarg * carg, httrackp * opt,
|
||||
for(j = 0; iisBogus[i][j] == a[j] && iisBogus[i][j] != '\0'; j++) ;
|
||||
if (iisBogus[i][j] == '\0'
|
||||
&& (a[j] == '\0' || a[j] == '/' || a[j] == '\\')) {
|
||||
/* j bytes matched, so j fit: copying j cannot overrun whatever the
|
||||
table holds, and the tail must survive untouched */
|
||||
memcpy(a, iisBogusReplace[i], (size_t) j);
|
||||
strncpy(a, iisBogusReplace[i], strlen(iisBogusReplace[i]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
.\"
|
||||
.\" This file is generated by man/makeman.sh; do not edit by hand.
|
||||
.\" SPDX-License-Identifier: GPL-3.0-or-later
|
||||
.TH httrack 1 "26 July 2026" "httrack website copier"
|
||||
.TH httrack 1 "23 July 2026" "httrack website copier"
|
||||
.SH NAME
|
||||
httrack \- offline browser : copy websites to a local directory
|
||||
.SH SYNOPSIS
|
||||
@@ -40,7 +40,6 @@ httrack \- offline browser : copy websites to a local directory
|
||||
[ \fB\-%N, \-\-delayed\-type\-check\fR ]
|
||||
[ \fB\-%D, \-\-cached\-delayed\-type\-check\fR ]
|
||||
[ \fB\-%M, \-\-mime\-html\fR ]
|
||||
[ \fB\-%Z, \-\-single\-file\fR ]
|
||||
[ \fB\-LN, \-\-long\-names[=N]\fR ]
|
||||
[ \fB\-KN, \-\-keep\-links[=N]\fR ]
|
||||
[ \fB\-x, \-\-replace\-external\fR ]
|
||||
@@ -199,8 +198,6 @@ delayed type check, don't make any link test but wait for files download to star
|
||||
cached delayed type check, don't wait for remote type during updates, to speedup them (%D0 wait, * %D1 don't wait) (\-\-cached\-delayed\-type\-check)
|
||||
.IP \-%M
|
||||
generate a RFC MIME\-encapsulated full\-archive (.mht) (\-\-mime\-html)
|
||||
.IP \-%Z
|
||||
after the mirror, rewrite each saved page with its stylesheets, scripts, images and fonts inlined as data: URIs, so any page opens by double\-click anywhere (links between pages stay relative; audio and video stay links); \-\-single\-file\-max\-size N caps each asset (default 10485760 bytes). %M is the better container where a Chromium\-family browser is a given: one archive, no base64 tax on text, a shared asset stored once (\-\-single\-file)
|
||||
.IP \-%t
|
||||
keep the original file extension, don't rewrite it from the MIME type (%t0 rewrite)
|
||||
.IP \-LN
|
||||
|
||||
@@ -48,7 +48,7 @@ htsserver_LDFLAGS = $(AM_LDFLAGS) $(LDFLAGS_PIE)
|
||||
|
||||
lib_LTLIBRARIES = libhttrack.la
|
||||
|
||||
htsserver_SOURCES = htsserver.c htsserver.h htsweb.c htsweb.h htsstats.h \
|
||||
htsserver_SOURCES = htsserver.c htsserver.h htsweb.c htsweb.h \
|
||||
htscmdline.c htscmdline.h \
|
||||
htsurlport.c htsurlport.h
|
||||
proxytrack_SOURCES = proxy/main.c \
|
||||
@@ -66,7 +66,7 @@ libhttrack_la_SOURCES = htscore.c htsparse.c htsback.c htscache.c \
|
||||
htscmdline.c htshelp.c htslib.c htsurlport.c htscoremain.c \
|
||||
htsname.c htsrobots.c htstools.c htswizard.c \
|
||||
htsalias.c htsthread.c htsindex.c htsbauth.c \
|
||||
htsmd5.c htscodec.c htswarc.c htssinglefile.c htsproxy.c htszlib.c htswrap.c htsconcat.c \
|
||||
htsmd5.c htscodec.c htswarc.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 \
|
||||
@@ -77,7 +77,7 @@ libhttrack_la_SOURCES = htscore.c htsparse.c htsback.c htscache.c \
|
||||
htshelp.h htsindex.h htslib.h htsurlport.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 htswarc.h htssinglefile.h htsproxy.h htszlib.h \
|
||||
htstools.h htswizard.h htswrap.h htscodec.h htswarc.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 \
|
||||
@@ -87,7 +87,7 @@ libhttrack_la_LIBADD = $(THREADS_LIBS) $(ZLIB_LIBS) $(BROTLI_LIBS) $(ZSTD_LIBS)
|
||||
libhttrack_la_CFLAGS = $(AM_CFLAGS) -DLIBHTTRACK_EXPORTS -DZLIB_CONST
|
||||
libhttrack_la_LDFLAGS = $(AM_LDFLAGS) -version-info $(VERSION_INFO)
|
||||
|
||||
EXTRA_DIST = httrack.h htsstats.h webhttrack \
|
||||
EXTRA_DIST = httrack.h webhttrack \
|
||||
version.rc \
|
||||
libhttrack.rc \
|
||||
httrack.rc \
|
||||
|
||||
@@ -123,10 +123,6 @@ const char *hts_optalias[][4] = {
|
||||
{"warc-cdxj", "-%rc", "single", ""},
|
||||
{"wacz", "-%rz", "single",
|
||||
"package the WARC archive, CDXJ index and pages as a WACZ file"},
|
||||
{"single-file", "-%Z", "single",
|
||||
"after the mirror, inline each page's assets as data: URIs"},
|
||||
{"single-file-max-size", "-%Zs", "param1",
|
||||
"per-asset cap for --single-file, in bytes (implies it; default 10485760)"},
|
||||
{"why", "-%Y", "param1",
|
||||
"explain which filter rule accepts or rejects a URL, then exit"},
|
||||
{"pause", "-%G", "param1",
|
||||
|
||||
@@ -142,12 +142,10 @@ struct cache_back_zip_entry {
|
||||
int compressionMethod;
|
||||
};
|
||||
|
||||
/* A corrupt cache can carry a field wider than ours; clipping it keeps the
|
||||
entry, where aborting would take the crawl down. */
|
||||
#define ZIP_READFIELD_STRING(line, value, refline, refvalue, refvalue_size) \
|
||||
do { \
|
||||
if (line[0] != '\0' && strfield2(line, refline)) { \
|
||||
(void) strclipbuff(refvalue, refvalue_size, value); \
|
||||
strlcpybuff(refvalue, value, refvalue_size); \
|
||||
line[0] = '\0'; \
|
||||
} \
|
||||
} while (0)
|
||||
@@ -1297,17 +1295,12 @@ char *readfile2(const char *fil, LLint * size) {
|
||||
}
|
||||
|
||||
/* Note: utf-8 */
|
||||
char *readfile_utf8(const char *fil) { return readfile2_utf8(fil, NULL); }
|
||||
|
||||
/* Note: utf-8 */
|
||||
char *readfile2_utf8(const char *fil, LLint *size) {
|
||||
char *readfile_utf8(const char *fil) {
|
||||
char *adr = NULL;
|
||||
char catbuff[CATBUFF_SIZE];
|
||||
const LLint len = fsize_utf8(fil);
|
||||
const size_t buflen = len >= 0 ? llint_to_size_t(len) : (size_t) -1;
|
||||
|
||||
if (size != NULL)
|
||||
*size = len;
|
||||
if (buflen != (size_t) -1) { // exists, and is addressable (see readfile2)
|
||||
FILE *const fp = FOPEN(fconv(catbuff, sizeof(catbuff), fil), "rb");
|
||||
|
||||
|
||||
@@ -1195,12 +1195,6 @@ int cache_legacy_refused_selftest(httrackp *opt, const char *dir) {
|
||||
|
||||
/* --- read-side corruption injection --------------------------------------- */
|
||||
|
||||
/* 100 'A's: a placeholder header line long enough to be overwritten by a
|
||||
forged, over-long X-StatusMessage of the same byte length. */
|
||||
#define CORRUPT_LONG_ETAG \
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" \
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
|
||||
/* canary read back intact after each corruption; victim gets the byte surgery
|
||||
*/
|
||||
#define CORRUPT_ADR "corrupt.example.com"
|
||||
@@ -1225,24 +1219,6 @@ static void corrupt_build(httrackp *opt) {
|
||||
selftest_close(&cache);
|
||||
}
|
||||
|
||||
/* Like corrupt_build, but the victim carries a 100-char Etag placeholder. */
|
||||
static void corrupt_build_longetag(httrackp *opt) {
|
||||
cache_back cache;
|
||||
|
||||
memset(corrupt_body_a, 'a', sizeof(corrupt_body_a) - 1);
|
||||
memset(corrupt_body_b, 'b', sizeof(corrupt_body_b) - 1);
|
||||
remove(reconcile_st_path(opt, "hts-cache/new.zip"));
|
||||
remove(reconcile_st_path(opt, "hts-cache/old.zip"));
|
||||
selftest_open_for_write(&cache, opt);
|
||||
store_entry(opt, &cache, CORRUPT_ADR, "/canary.html", "canary.html", 200,
|
||||
"OK", "text/html", "utf-8", "", "", "", "", corrupt_body_a,
|
||||
strlen(corrupt_body_a));
|
||||
store_entry(opt, &cache, CORRUPT_ADR, "/victim.html", "victim.html", 200,
|
||||
"OK", "text/html", "utf-8", "", CORRUPT_LONG_ETAG, "", "",
|
||||
corrupt_body_b, strlen(corrupt_body_b));
|
||||
selftest_close(&cache);
|
||||
}
|
||||
|
||||
/* Like corrupt_build, but the victim carries a 20-char Etag whose header line
|
||||
is later overwritten with a forged oversized X-Size (same byte length). */
|
||||
static void corrupt_build_etag(httrackp *opt) {
|
||||
@@ -1427,48 +1403,6 @@ static int corrupt_expect_disk_header(httrackp *opt, LLint wantsize,
|
||||
return fail;
|
||||
}
|
||||
|
||||
/* An over-long field from a foreign cache must clip, not abort: the entry
|
||||
still reads, clipped to capacity, and the canary survives. */
|
||||
static int corrupt_expect_victim_clipped(httrackp *opt, size_t wantmsg,
|
||||
size_t wantlastmod, const char *what) {
|
||||
cache_back cache;
|
||||
htsblk v, c;
|
||||
char BIGSTK lv[HTS_URLMAXSIZE * 2];
|
||||
char BIGSTK lc[HTS_URLMAXSIZE * 2];
|
||||
int fail = 0;
|
||||
|
||||
selftest_open_for_read(&cache, opt);
|
||||
lv[0] = lc[0] = '\0';
|
||||
v = cache_readex(opt, &cache, CORRUPT_ADR, "/victim.html", "", lv, NULL, 1);
|
||||
if (v.statuscode != 200) {
|
||||
fprintf(stderr, "%s: %s: status %d, expected 200\n", selftest_tag, what,
|
||||
v.statuscode);
|
||||
fail++;
|
||||
}
|
||||
if (wantmsg != (size_t) -1 && strlen(v.msg) != wantmsg) {
|
||||
fprintf(stderr, "%s: %s: msg len %u, expected %u\n", selftest_tag, what,
|
||||
(unsigned) strlen(v.msg), (unsigned) wantmsg);
|
||||
fail++;
|
||||
}
|
||||
if (wantlastmod != (size_t) -1 && strlen(v.lastmodified) != wantlastmod) {
|
||||
fprintf(stderr, "%s: %s: lastmodified len %u, expected %u\n", selftest_tag,
|
||||
what, (unsigned) strlen(v.lastmodified), (unsigned) wantlastmod);
|
||||
fail++;
|
||||
}
|
||||
c = cache_readex(opt, &cache, CORRUPT_ADR, "/canary.html", "", lc, NULL, 1);
|
||||
if (c.statuscode != 200) {
|
||||
fprintf(stderr, "%s: %s: canary tainted (status %d)\n", selftest_tag, what,
|
||||
c.statuscode);
|
||||
fail++;
|
||||
}
|
||||
if (v.adr != NULL)
|
||||
freet(v.adr);
|
||||
if (c.adr != NULL)
|
||||
freet(c.adr);
|
||||
selftest_close(&cache);
|
||||
return fail;
|
||||
}
|
||||
|
||||
/* One zip corruption case: build, patch, then check victim+canary in-session.
|
||||
*/
|
||||
static int corrupt_case_zip(httrackp *opt, const char *pat, const char *rep,
|
||||
@@ -1505,31 +1439,6 @@ int cache_corruption_selftest(httrackp *opt, const char *dir) {
|
||||
failures += corrupt_expect_victim(opt, "Cache Read Error : Read Data",
|
||||
"garbled deflate stream");
|
||||
|
||||
/* A corrupt cache can hold a field wider than ours. Clipping keeps the
|
||||
entry; aborting would take the crawl down. Overwrite the placeholder Etag
|
||||
line in place, same byte length, so the zip offsets stay intact. */
|
||||
corrupt_build_longetag(opt);
|
||||
corrupt_patch(opt, "Etag: " CORRUPT_LONG_ETAG, 106,
|
||||
"X-StatusMessage: " /* 17 + 89 = 106 */
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
1, 1);
|
||||
failures +=
|
||||
corrupt_expect_victim_clipped(opt, sizeof(((htsblk *) 0)->msg) - 1,
|
||||
(size_t) -1, "over-long X-StatusMessage");
|
||||
|
||||
/* lastmodified[64] is narrower than msg[80]: one hardcoded clip length
|
||||
cannot satisfy both. */
|
||||
corrupt_build_longetag(opt);
|
||||
corrupt_patch(opt, "Etag: " CORRUPT_LONG_ETAG, 106,
|
||||
"Last-Modified: " /* 15 + 91 = 106 */
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
1, 1);
|
||||
failures += corrupt_expect_victim_clipped(
|
||||
opt, (size_t) -1, sizeof(((htsblk *) 0)->lastmodified) - 1,
|
||||
"over-long Last-Modified");
|
||||
|
||||
/* An X-Size above INT_MAX is positive as int64 (slips a bare sign check) but
|
||||
truncates negative in the (int) cast the malloc uses: a wraparound alloc.
|
||||
cache_add asserts size fits an int, so such a value only reaches the reader
|
||||
|
||||
@@ -40,7 +40,6 @@ Please visit our Website: http://www.httrack.com
|
||||
/* File defs */
|
||||
#include "htscore.h"
|
||||
#include "htswarc.h"
|
||||
#include "htssinglefile.h"
|
||||
|
||||
/* specific definitions */
|
||||
#include "htsbase.h"
|
||||
@@ -2183,10 +2182,6 @@ int httpmirror(char *url1, httrackp * opt) {
|
||||
}
|
||||
// fin purge!
|
||||
|
||||
/* --single-file: inline each page's assets now the tree is final, after the
|
||||
purge has deleted whatever this run dropped. */
|
||||
singlefile_process_mirror(opt);
|
||||
|
||||
// Indexation
|
||||
if (opt->kindex)
|
||||
index_finish(StringBuff(opt->path_html), opt->kindex);
|
||||
@@ -3648,10 +3643,6 @@ HTSEXT_API int copy_htsopt(const httrackp * from, httrackp * to) {
|
||||
to->warc_cdx = from->warc_cdx;
|
||||
to->warc_wacz = from->warc_wacz;
|
||||
|
||||
to->single_file = from->single_file;
|
||||
if (from->single_file_max_size > 0)
|
||||
to->single_file_max_size = from->single_file_max_size;
|
||||
|
||||
if (from->pause_max_ms > 0) {
|
||||
to->pause_min_ms = from->pause_min_ms;
|
||||
to->pause_max_ms = from->pause_max_ms;
|
||||
|
||||
@@ -409,8 +409,6 @@ char *readfile2(const char *fil, LLint * size);
|
||||
|
||||
char *readfile_utf8(const char *fil);
|
||||
|
||||
char *readfile2_utf8(const char *fil, LLint *size);
|
||||
|
||||
char *readfile_or(const char *fil, const char *defaultdata);
|
||||
|
||||
/* Backing (download-slot) scheduler. Operate on the back[] ring (struct_back).
|
||||
|
||||
@@ -1795,36 +1795,6 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
|
||||
StringCopy(opt->warc_file, WARC_AUTONAME);
|
||||
}
|
||||
break;
|
||||
case 'Z': // single-file: inline each page's assets as data: URIs
|
||||
if (*(com + 1) == 's') { // --single-file-max-size N
|
||||
com++;
|
||||
if ((na + 1 >= argc) || (argv[na + 1][0] == '-')) {
|
||||
HTS_PANIC_PRINTF(
|
||||
"Option single-file-max-size needs a blank "
|
||||
"space and a size");
|
||||
htsmain_free();
|
||||
return -1;
|
||||
}
|
||||
na++;
|
||||
{ // reject non-numeric/negative/overflow; keep the default
|
||||
char *end;
|
||||
LLint v;
|
||||
|
||||
errno = 0;
|
||||
v = strtoll(argv[na], &end, 10);
|
||||
if (isdigit((unsigned char) argv[na][0]) && *end == '\0' &&
|
||||
errno != ERANGE && v > 0)
|
||||
opt->single_file_max_size = v;
|
||||
}
|
||||
opt->single_file = HTS_TRUE;
|
||||
} else {
|
||||
opt->single_file = HTS_TRUE;
|
||||
if (*(com + 1) == '0') {
|
||||
opt->single_file = HTS_FALSE;
|
||||
com++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'Y': // why: explain the filter verdict for a URL, no crawl
|
||||
if ((na + 1 >= argc) || (argv[na + 1][0] == '-')) {
|
||||
HTS_PANIC_PRINTF("Option why needs a blank space and a URL");
|
||||
|
||||
@@ -433,8 +433,7 @@ void help_catchurl(const char *dest_path) {
|
||||
}
|
||||
// former URL!
|
||||
{
|
||||
/* url and dest are each HTS_URLMAXSIZE*2, plus the POSTTOK marker */
|
||||
char BIGSTK finalurl[HTS_URLMAXSIZE * 4 + 32];
|
||||
char BIGSTK finalurl[HTS_URLMAXSIZE * 2];
|
||||
|
||||
inplace_escape_check_url(dest, sizeof(dest));
|
||||
snprintf(finalurl, sizeof(finalurl), "%s" POSTTOK "file:%s", url, dest);
|
||||
@@ -535,13 +534,6 @@ void help(const char *app, int more) {
|
||||
infomsg
|
||||
(" %D cached delayed type check, don't wait for remote type during updates, to speedup them (%D0 wait, * %D1 don't wait)");
|
||||
infomsg(" %M generate a RFC MIME-encapsulated full-archive (.mht)");
|
||||
infomsg(" %Z after the mirror, rewrite each saved page with its "
|
||||
"stylesheets, scripts, images and fonts inlined as data: URIs, so "
|
||||
"any page opens by double-click anywhere (links between pages stay "
|
||||
"relative; audio and video stay links); --single-file-max-size N "
|
||||
"caps each asset (default 10485760 bytes). %M is the better "
|
||||
"container where a Chromium-family browser is a given: one archive, "
|
||||
"no base64 tax on text, a shared asset stored once");
|
||||
infomsg(" %t keep the original file extension, don't rewrite it from the "
|
||||
"MIME type (%t0 rewrite)");
|
||||
infomsg
|
||||
|
||||
12
src/htslib.c
12
src/htslib.c
@@ -37,7 +37,6 @@ Please visit our Website: http://www.httrack.com
|
||||
|
||||
#include "htscore.h"
|
||||
#include "htswarc.h"
|
||||
#include "htssinglefile.h"
|
||||
|
||||
/* specific definitions */
|
||||
#include "htsbase.h"
|
||||
@@ -2538,6 +2537,15 @@ void fil_simplifie(char *f) {
|
||||
}
|
||||
}
|
||||
|
||||
void htsblk_failf(htsblk *r, const char *fmt, ...) {
|
||||
va_list args;
|
||||
|
||||
va_start(args, fmt);
|
||||
// deliberate clip: the reason is quoted from a remote reply
|
||||
(void) vslprintfbuff(r->msg, sizeof(r->msg), fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
// fermer liaison fichier ou socket
|
||||
void deletehttp(htsblk * r) {
|
||||
#if HTS_DEBUG_CLOSESOCK
|
||||
@@ -6012,8 +6020,6 @@ HTSEXT_API httrackp *hts_create_opt(void) {
|
||||
StringCopy(opt->cookies_file, "");
|
||||
StringCopy(opt->warc_file, "");
|
||||
opt->warc_max_size = 0; /* no rotation unless --warc-max-size sets it */
|
||||
opt->single_file = HTS_FALSE;
|
||||
opt->single_file_max_size = SINGLEFILE_DEFAULT_MAX_SIZE;
|
||||
StringCopy(opt->why_url, "");
|
||||
opt->pause_min_ms = 0;
|
||||
opt->pause_max_ms = 0;
|
||||
|
||||
@@ -201,8 +201,7 @@ T_SOC newhttp_addr(httrackp *opt, const char *iadr, htsblk *retour, int port,
|
||||
int waitconnect, int addr_index, int *addr_count);
|
||||
/* Clips the formatted failure reason into r->msg, which also round-trips
|
||||
through the cache as X-StatusMessage. Leaves r->statuscode to the caller. */
|
||||
#define htsblk_failf(R, ...) \
|
||||
slprintfbuff_clip((R)->msg, sizeof((R)->msg), __VA_ARGS__)
|
||||
void htsblk_failf(htsblk *r, const char *fmt, ...) HTS_PRINTF_FUN(2, 3);
|
||||
|
||||
HTS_INLINE void deletehttp(htsblk * r);
|
||||
HTS_INLINE int deleteaddr(htsblk * r);
|
||||
|
||||
@@ -547,12 +547,6 @@ struct httrackp {
|
||||
archive. Tail: ABI */
|
||||
hts_boolean warc_wacz; /**< --wacz: package archive+index+pages as a WACZ zip
|
||||
(implies --warc + --warc-cdx). Tail: ABI */
|
||||
hts_boolean single_file; /**< --single-file: once the mirror is done, rewrite
|
||||
each saved page with its assets inlined as
|
||||
data: URIs. Tail: ABI */
|
||||
LLint single_file_max_size; /**< --single-file-max-size: per-asset cap in
|
||||
bytes; a bigger asset stays a link.
|
||||
Tail: ABI */
|
||||
};
|
||||
|
||||
/* Running statistics for a mirror. */
|
||||
|
||||
@@ -460,25 +460,6 @@ static HTS_INLINE HTS_UNUSED const char *htsbuff_str(const htsbuff *b) {
|
||||
return b->buf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy src into dest (capacity size, NUL included), truncating to fit and
|
||||
* always NUL-terminating. Unlike strlcpybuff() it never aborts, so it suits a
|
||||
* value read back from a cache, a header or the wire, where refusing the whole
|
||||
* record is worse than keeping a clipped one. Returns HTS_TRUE if it all fit;
|
||||
* callers that clip on purpose ignore that, so it is not HTS_CHECK_RESULT.
|
||||
*/
|
||||
static HTS_INLINE HTS_UNUSED hts_boolean strclipbuff(char *dest, size_t size,
|
||||
const char *src) {
|
||||
size_t len, copy;
|
||||
|
||||
assertf(dest != NULL && src != NULL && size != 0);
|
||||
len = strlen(src);
|
||||
copy = len < size ? len : size - 1;
|
||||
memcpy(dest, src, copy);
|
||||
dest[copy] = '\0';
|
||||
return copy == len ? HTS_TRUE : HTS_FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callers that deliberately ignore truncation use this instead of
|
||||
* slprintfbuff(), so it is not HTS_CHECK_RESULT.
|
||||
@@ -488,9 +469,6 @@ static HTS_INLINE HTS_UNUSED HTS_PRINTF_FUN(3, 0) hts_boolean
|
||||
int ret;
|
||||
|
||||
assertf(dest != NULL && size != 0);
|
||||
/* a vsnprintf failing outright may write nothing at all, leaving whatever
|
||||
the caller had on the stack for it to publish */
|
||||
dest[0] = '\0';
|
||||
ret = vsnprintf(dest, size, fmt, args);
|
||||
/* pre-C99 runtimes (msvcrt _vsnprintf) return -1 and do not terminate */
|
||||
dest[size - 1] = '\0';
|
||||
@@ -514,20 +492,6 @@ static HTS_INLINE HTS_UNUSED HTS_CHECK_RESULT HTS_PRINTF_FUN(3, 4) hts_boolean
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* slprintfbuff() for diagnostics quoting remote or client text, which are
|
||||
* meant to be clipped: nothing to act on, hence not HTS_CHECK_RESULT. A (void)
|
||||
* cast on slprintfbuff() is no substitute, GCC warns through it.
|
||||
*/
|
||||
static HTS_INLINE HTS_UNUSED HTS_PRINTF_FUN(3, 4) void slprintfbuff_clip(
|
||||
char *dest, size_t size, const char *fmt, ...) {
|
||||
va_list args;
|
||||
|
||||
va_start(args, fmt);
|
||||
(void) vslprintfbuff(dest, size, fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* slprintfbuff() over the in-scope array ARR (capacity = sizeof(ARR)).
|
||||
* On GCC/Clang a pointer is a compile error; use slprintfbuff() for those.
|
||||
|
||||
@@ -60,7 +60,6 @@ Please visit our Website: http://www.httrack.com
|
||||
#include "htscodec.h"
|
||||
#include "htsproxy.h"
|
||||
#include "htswarc.h"
|
||||
#include "htssinglefile.h"
|
||||
#if HTS_USEZLIB
|
||||
#include "htszlib.h"
|
||||
#endif
|
||||
@@ -625,66 +624,6 @@ static int string_safety_selftests(void) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* strclipbuff: truncate-and-report, never abort. Same canary shape; the
|
||||
destination is poisoned before every call so a case cannot pass on the
|
||||
previous one's bytes. */
|
||||
{
|
||||
struct {
|
||||
char dst[8];
|
||||
char canary[8];
|
||||
} s;
|
||||
|
||||
memset(&s, '#', sizeof(s));
|
||||
#define POISON_DST() memset(s.dst, '#', sizeof(s.dst))
|
||||
|
||||
/* well under capacity: no padding, nothing eaten off the end */
|
||||
POISON_DST();
|
||||
if (!strclipbuff(s.dst, sizeof(s.dst), "abc") || strcmp(s.dst, "abc") != 0)
|
||||
return 1;
|
||||
|
||||
/* exact fit: capacity - 1 characters plus the NUL */
|
||||
POISON_DST();
|
||||
if (!strclipbuff(s.dst, sizeof(s.dst), "1234567") ||
|
||||
strcmp(s.dst, "1234567") != 0)
|
||||
return 1;
|
||||
|
||||
/* one over, then far over: clipped, terminated, reported. The expected
|
||||
bytes differ from the case above, so a write-nothing implementation
|
||||
cannot pass on the leftovers. */
|
||||
POISON_DST();
|
||||
if (strclipbuff(s.dst, sizeof(s.dst), "abcdefgh") ||
|
||||
strcmp(s.dst, "abcdefg") != 0)
|
||||
return 1;
|
||||
POISON_DST();
|
||||
if (strclipbuff(s.dst, sizeof(s.dst), "0123456789abcdef") ||
|
||||
strcmp(s.dst, "0123456") != 0)
|
||||
return 1;
|
||||
|
||||
/* degenerate capacity 1: only the NUL fits. Capacity 2 pins the boundary
|
||||
between that and the sizes above: one character plus the NUL. */
|
||||
POISON_DST();
|
||||
if (strclipbuff(s.dst, 1, "x") || s.dst[0] != '\0')
|
||||
return 1;
|
||||
POISON_DST();
|
||||
if (strclipbuff(s.dst, 2, "yz") || strcmp(s.dst, "y") != 0)
|
||||
return 1;
|
||||
|
||||
/* the empty string fits any non-zero capacity */
|
||||
POISON_DST();
|
||||
if (!strclipbuff(s.dst, sizeof(s.dst), "") || s.dst[0] != '\0')
|
||||
return 1;
|
||||
|
||||
/* a byte over 0x7f must not end the copy early */
|
||||
POISON_DST();
|
||||
if (!strclipbuff(s.dst, sizeof(s.dst), "\xff\xfe") ||
|
||||
strcmp(s.dst, "\xff\xfe") != 0)
|
||||
return 1;
|
||||
#undef POISON_DST
|
||||
|
||||
if (memcmp(s.canary, "########", sizeof(s.canary)) != 0)
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* htsblk_failf: clips a reason quoted from a remote reply into msg[] and
|
||||
touches nothing else in the block */
|
||||
{
|
||||
@@ -1586,7 +1525,7 @@ static int st_hashtable(httrackp *opt, int argc, char **argv) {
|
||||
size_t i;
|
||||
for (i = bench[loop].offset; i < (size_t) count;
|
||||
i += bench[loop].modulus) {
|
||||
int result = 0; /* no final else: an unknown type reports failure */
|
||||
int result;
|
||||
FMT();
|
||||
if (bench[loop].type == DO_ADD || bench[loop].type == DO_DRY_ADD) {
|
||||
size_t k;
|
||||
@@ -1741,20 +1680,6 @@ static int st_copyopt(httrackp *opt, int argc, char **argv) {
|
||||
if (strcmp(StringBuff(to->warc_file), "run.warc.gz") != 0)
|
||||
err = 1;
|
||||
|
||||
/* single_file pair: the cap is guarded by >0, so an unset source must not
|
||||
overwrite the default the target already carries */
|
||||
from->single_file = HTS_TRUE;
|
||||
from->single_file_max_size = 4096;
|
||||
to->single_file = HTS_FALSE;
|
||||
to->single_file_max_size = SINGLEFILE_DEFAULT_MAX_SIZE;
|
||||
copy_htsopt(from, to);
|
||||
if (!to->single_file || to->single_file_max_size != 4096)
|
||||
err = 1;
|
||||
from->single_file_max_size = 0;
|
||||
copy_htsopt(from, to);
|
||||
if (to->single_file_max_size != 4096)
|
||||
err = 1;
|
||||
|
||||
/* #185 pause pair: copied when enabled (max>0), the 0 sentinel skips */
|
||||
from->pause_min_ms = 5000;
|
||||
from->pause_max_ms = 10000;
|
||||
@@ -4890,520 +4815,6 @@ static int st_warc_wacz(httrackp *opt, int argc, char **argv) {
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/* --single-file */
|
||||
/* ------------------------------------------------------------ */
|
||||
|
||||
static int sf_err = 0;
|
||||
|
||||
/* Set when the filesystem accepted a ':' in a name; Windows never does. */
|
||||
static hts_boolean sf_colon_ok = HTS_FALSE;
|
||||
|
||||
static void sf_check(int ok, const char *what) {
|
||||
if (!ok) {
|
||||
fprintf(stderr, "singlefile: %s\n", what);
|
||||
sf_err++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Write rel (a '/'-separated path under dir), creating the directories.
|
||||
Returns HTS_FALSE if the name is one the filesystem will not take. */
|
||||
static hts_boolean sf_try_put(const char *dir, const char *rel,
|
||||
const void *data, size_t len) {
|
||||
char BIGSTK path[HTS_URLMAXSIZE * 2];
|
||||
char catbuff[CATBUFF_SIZE];
|
||||
FILE *fp;
|
||||
|
||||
fconcat(path, sizeof(path), dir, rel);
|
||||
structcheck_utf8(path);
|
||||
fp = FOPEN(fconv(catbuff, sizeof(catbuff), path), "wb");
|
||||
if (fp == NULL)
|
||||
return HTS_FALSE;
|
||||
assertf(len == 0 || fwrite(data, 1, len, fp) == len);
|
||||
fclose(fp);
|
||||
return HTS_TRUE;
|
||||
}
|
||||
|
||||
static void sf_put(const char *dir, const char *rel, const void *data,
|
||||
size_t len) {
|
||||
assertf(sf_try_put(dir, rel, data, len));
|
||||
}
|
||||
|
||||
/* Number of times needle occurs in hay. */
|
||||
static int sf_count(const char *hay, const char *needle) {
|
||||
const size_t l = strlen(needle);
|
||||
int n = 0;
|
||||
const char *p = hay;
|
||||
|
||||
while ((p = strstr(p, needle)) != NULL) {
|
||||
n++;
|
||||
p += l;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* The base64 payload following the first occurrence of prefix, up to the first
|
||||
byte outside the base64 alphabet. NULL if prefix is absent. */
|
||||
static const char *sf_payload(const char *hay, const char *prefix,
|
||||
size_t *len) {
|
||||
const char *p = strstr(hay, prefix);
|
||||
size_t n = 0;
|
||||
|
||||
if (p == NULL)
|
||||
return NULL;
|
||||
p += strlen(prefix);
|
||||
while (p[n] != '\0' && (isalnum((unsigned char) p[n]) || p[n] == '+' ||
|
||||
p[n] == '/' || p[n] == '='))
|
||||
n++;
|
||||
*len = n;
|
||||
return p;
|
||||
}
|
||||
|
||||
/* Independent base64 decoder: the round-trip check must not lean on code64().
|
||||
*/
|
||||
static unsigned char *sf_unb64(const char *s, size_t len, size_t *outlen) {
|
||||
static const char alpha[] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
unsigned char *out = (unsigned char *) malloct(len / 4 * 3 + 4);
|
||||
unsigned int acc = 0;
|
||||
size_t i, n = 0;
|
||||
int bits = 0;
|
||||
|
||||
if (out == NULL)
|
||||
return NULL;
|
||||
for (i = 0; i < len; i++) {
|
||||
const char *const p = s[i] != '\0' ? strchr(alpha, s[i]) : NULL;
|
||||
|
||||
if (s[i] == '=')
|
||||
break;
|
||||
if (p == NULL) {
|
||||
freet(out);
|
||||
return NULL;
|
||||
}
|
||||
acc = (acc << 6) | (unsigned int) (p - alpha);
|
||||
bits += 6;
|
||||
if (bits >= 8) {
|
||||
bits -= 8;
|
||||
out[n++] = (unsigned char) ((acc >> bits) & 0xff);
|
||||
}
|
||||
}
|
||||
*outlen = n;
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Decoded payload of the first data: URI with that MIME, as a NUL-terminated
|
||||
buffer the caller freet()s. NULL when absent or undecodable. */
|
||||
static char *sf_decode(const char *hay, const char *mime, size_t *outlen) {
|
||||
char prefix[128];
|
||||
size_t len = 0, dlen = 0;
|
||||
const char *b64;
|
||||
unsigned char *raw;
|
||||
|
||||
snprintf(prefix, sizeof(prefix), "data:%s;base64,", mime);
|
||||
b64 = sf_payload(hay, prefix, &len);
|
||||
if (b64 == NULL)
|
||||
return NULL;
|
||||
raw = sf_unb64(b64, len, &dlen);
|
||||
if (raw == NULL)
|
||||
return NULL;
|
||||
raw[dlen] = '\0';
|
||||
if (outlen != NULL)
|
||||
*outlen = dlen;
|
||||
return (char *) raw;
|
||||
}
|
||||
|
||||
/* How many data: URIs of that MIME nest inside each other, starting at hay. */
|
||||
static int sf_nesting(const char *hay, const char *mime) {
|
||||
char *cur = strdupt(hay);
|
||||
int n = 0;
|
||||
|
||||
while (cur != NULL) {
|
||||
char *const inner = sf_decode(cur, mime, NULL);
|
||||
|
||||
freet(cur);
|
||||
cur = inner;
|
||||
if (inner != NULL)
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Above the tag parser's attribute limit, so the fixture crosses it. */
|
||||
#define SF_ST_MAX_ATTRS 64
|
||||
|
||||
/* The 12-byte asset: high bytes and an embedded NUL, so a text-shaped copy
|
||||
would be caught. */
|
||||
static const char sf_png[] = "\x89PNG\r\n\x1a\n\x00\x01\x02\xff";
|
||||
#define SF_PNG_LEN 12
|
||||
|
||||
static const char sf_page[] =
|
||||
"<html><head>\n"
|
||||
"<link rel=\"stylesheet\" href=\"css/main.css\">\n"
|
||||
"<link rel=\"canonical\" href=\"other.html\">\n"
|
||||
"<title>t</title>\n"
|
||||
"<style>body { background: url(\"img/a%20b.png\"); }</style>\n"
|
||||
"</head><body>\n"
|
||||
"<img src=\"img/a%20b.png\" srcset=\"img/a%20b.png 1x, img/big.png 2x\">\n"
|
||||
"<link rel=\"icon\" href=\"icon.png\">\n"
|
||||
"<link rel=\"preload\" as=\"font\" href=\"font/f.woff2\">\n"
|
||||
"<img src=\"data:image/gif;base64,QUJD\">\n"
|
||||
/* Each has a real file where its guard's removal would land it; without
|
||||
that they stay links either way, the target merely being absent. */
|
||||
"<img src=\"http://example.com/x.png\">\n"
|
||||
"<img src=\"//example.com/x.png\">\n"
|
||||
"<input type=\"image\" src=\"img/in.png\">\n"
|
||||
/* Lazy loading: src is the placeholder, the real image rides data-src. */
|
||||
"<img src=\"img/ph.png\" data-src=\"img/lz.png\" "
|
||||
"data-srcset=\"img/lz2.png 2x\" lowsrc=\"img/low.png\">\n"
|
||||
"<object data=\"img/ob.png\"></object>\n"
|
||||
"<embed src=\"img/em.png\">\n"
|
||||
"<img data-src=\"other.html\">\n"
|
||||
/* What a first pass emits: re-resolving it is what a second pass must not
|
||||
do, and the fallback type would inline whatever the walk found. */
|
||||
"<link rel=\"stylesheet\" href=\"data:text/css;base64,QUJD\">\n"
|
||||
"<video poster=\"img/po.png\" controls>"
|
||||
"<source src=\"v.mp4\" type=\"video/mp4\"></video>\n"
|
||||
"<svg><image href=\"img/sv.png\"/></svg>\n"
|
||||
"<table background=\"img/bg.png\"><tr><td>x</td></tr></table>\n"
|
||||
/* The second is what bites: drop the clamp and its leading ".." lands it
|
||||
back on <root>/img/a b.png. The first can only 404 either way. */
|
||||
"<img src=\"../escape.png\">\n"
|
||||
"<img src=\"../img/a%20b.png\">\n"
|
||||
"<a href=\"img/a%20b.png\">link</a>\n"
|
||||
"<script src=\"js/app.js\"></script>\n"
|
||||
"<script>var s = \"</scripting>\"; var t = \"<img src='img/a%20b.png'>\";"
|
||||
"</script>\n"
|
||||
"<img src=\"missing.png\" >\n"
|
||||
"<!--><img src=\"img/a%20b.png\">\n"
|
||||
"<div style=\"background:url(img/a%20b.png)\"></div>\n"
|
||||
"<div style='content:\"x\"; background:url(img/a%20b.png)'></div>\n"
|
||||
"</body></html>\n";
|
||||
|
||||
/* Lay a small mirror down under root. */
|
||||
static void sf_fixture(const char *root) {
|
||||
/* The over-cap url() is what drives the rebase fallback: a reference an
|
||||
inlined stylesheet could not embed has to come out relative to the page,
|
||||
not to the stylesheet, or it dangles. */
|
||||
static const char css[] =
|
||||
"@import \"sub/nested.css\";\n"
|
||||
"@import url(\"sub/two.css\");\n"
|
||||
"@import \"a\\\"url(../img/a b.png)b.css\";\n"
|
||||
"@font-face { font-family: f; src: url(../font/f.woff2); }\n"
|
||||
"body { background: url(../img/a b.png); }\n"
|
||||
"div { background: url(../img/big.png); }\n"
|
||||
"/* url(../img/never.png) */\n";
|
||||
static const char nested[] = "div { background: url(../../img/a b.png); }\n";
|
||||
static const char two[] = "p { background: url(../../img/a b.png); }\n";
|
||||
static const char deep[] =
|
||||
"<html><head><link rel=\"stylesheet\" href=\"../../css/main.css\">\n"
|
||||
"</head><body>d</body></html>\n";
|
||||
static const char js[] = "var app = 1;\n";
|
||||
char big[4096];
|
||||
|
||||
memset(big, 'B', sizeof(big));
|
||||
sf_put(root, "page.html", sf_page, sizeof(sf_page) - 1);
|
||||
sf_put(root, "deep/sub/page.html", deep, sizeof(deep) - 1);
|
||||
sf_put(root, "other.html", "<html>o</html>", 14);
|
||||
sf_put(root, "css/main.css", css, sizeof(css) - 1);
|
||||
sf_put(root, "css/sub/nested.css", nested, sizeof(nested) - 1);
|
||||
sf_put(root, "css/sub/two.css", two, sizeof(two) - 1);
|
||||
sf_put(root, "js/app.js", js, sizeof(js) - 1);
|
||||
sf_put(root, "img/a b.png", sf_png, SF_PNG_LEN);
|
||||
sf_put(root, "img/big.png", big, sizeof(big));
|
||||
sf_put(root, "img/in.png", sf_png, SF_PNG_LEN);
|
||||
sf_put(root, "img/po.png", sf_png, SF_PNG_LEN);
|
||||
sf_put(root, "img/sv.png", sf_png, SF_PNG_LEN);
|
||||
sf_put(root, "img/bg.png", sf_png, SF_PNG_LEN);
|
||||
sf_put(root, "img/ph.png", sf_png, SF_PNG_LEN);
|
||||
sf_put(root, "img/lz.png", sf_png, SF_PNG_LEN);
|
||||
sf_put(root, "img/lz2.png", sf_png, SF_PNG_LEN);
|
||||
sf_put(root, "img/low.png", sf_png, SF_PNG_LEN);
|
||||
sf_put(root, "img/ob.png", sf_png, SF_PNG_LEN);
|
||||
sf_put(root, "img/em.png", sf_png, SF_PNG_LEN);
|
||||
sf_put(root, "icon.png", sf_png, SF_PNG_LEN);
|
||||
sf_put(root, "font/f.woff2", "wOF2\x00\x01", 6);
|
||||
/* Where a guard's removal would land each reference that has to stay a
|
||||
link. The colon-bearing two are impossible on Windows, where the scheme
|
||||
guard then only gets the weaker "the link survived" check. */
|
||||
sf_put(root, "img/never.png", sf_png, SF_PNG_LEN);
|
||||
sf_put(root, "example.com/x.png", sf_png, SF_PNG_LEN);
|
||||
sf_colon_ok =
|
||||
sf_try_put(root, "http:/example.com/x.png", sf_png, SF_PNG_LEN) &&
|
||||
sf_try_put(root, "data:text/css;base64,QUJD", "p{}", 3);
|
||||
{ /* More attributes than the tag parser records, with a '>' inside a quoted
|
||||
value: the give-up path must not rescan quote-blind. */
|
||||
String wide = STRING_EMPTY;
|
||||
int n;
|
||||
|
||||
StringCopy(wide, "<html><body><p");
|
||||
for (n = 0; n <= SF_ST_MAX_ATTRS; n++) {
|
||||
char one[32];
|
||||
|
||||
assertf(sprintfbuff(one, " a%d=1", n));
|
||||
StringCat(wide, one);
|
||||
}
|
||||
StringCat(wide,
|
||||
" title=\"> <img src=img/a%20b.png> \">end</p></body></html>");
|
||||
sf_put(root, "wide.html", StringBuff(wide), StringLength(wide));
|
||||
StringFree(wide);
|
||||
}
|
||||
sf_put(root, "v.mp4",
|
||||
"\x00\x00\x00\x18"
|
||||
"ftypisom",
|
||||
12);
|
||||
}
|
||||
|
||||
/* -#test=singlefile <dir>: rewrite a hand-built mirror and check what gets
|
||||
inlined, what must keep its link, the per-asset cap, and idempotence. */
|
||||
static int st_singlefile(httrackp *opt, int argc, char **argv) {
|
||||
char BIGSTK root[HTS_URLMAXSIZE];
|
||||
char BIGSTK page[HTS_URLMAXSIZE * 2];
|
||||
const LLint saved_cap = opt->single_file_max_size;
|
||||
char *out, *css, *nested;
|
||||
size_t outlen = 0, len = 0;
|
||||
|
||||
if (argc < 1) {
|
||||
fprintf(stderr, "singlefile: needs a writable directory\n");
|
||||
return 1;
|
||||
}
|
||||
sf_err = 0;
|
||||
sf_put(argv[0], "escape.png", sf_png,
|
||||
SF_PNG_LEN); /* just outside the mirror */
|
||||
fconcat(root, sizeof(root), argv[0], "mirror/");
|
||||
sf_fixture(root);
|
||||
fconcat(page, sizeof(page), root, "page.html");
|
||||
|
||||
/* Cap between the small assets and big.png. */
|
||||
opt->single_file_max_size = 1024;
|
||||
sf_check(singlefile_rewrite_file(opt, root, page),
|
||||
"first pass changed nothing");
|
||||
out = readfile_utf8(page);
|
||||
assertf(out != NULL);
|
||||
|
||||
/* Inlined, and the payload is the file's exact bytes. */
|
||||
{
|
||||
char *img = sf_decode(out, "image/png", &len);
|
||||
|
||||
sf_check(img != NULL && len == SF_PNG_LEN &&
|
||||
memcmp(img, sf_png, SF_PNG_LEN) == 0,
|
||||
"image payload does not round-trip");
|
||||
freet(img);
|
||||
}
|
||||
{
|
||||
char *js = sf_decode(out, "application/x-javascript", &len);
|
||||
|
||||
sf_check(js != NULL && len == 13 && memcmp(js, "var app = 1;\n", 13) == 0,
|
||||
"script payload does not round-trip");
|
||||
freet(js);
|
||||
}
|
||||
sf_check(strstr(out, "href=\"data:text/css;base64,") != NULL,
|
||||
"stylesheet not inlined into the link");
|
||||
{
|
||||
char *font = sf_decode(out, "font/woff2", &len);
|
||||
|
||||
sf_check(font != NULL && len == 6 && memcmp(font, "wOF2\x00\x01", 6) == 0,
|
||||
"rel=preload font payload does not round-trip");
|
||||
freet(font);
|
||||
}
|
||||
|
||||
/* Every other (tag, attribute) rule in the table. */
|
||||
sf_check(strstr(out, "icon.png") == NULL, "rel=icon not inlined");
|
||||
sf_check(strstr(out, "img/in.png") == NULL, "input src not inlined");
|
||||
sf_check(strstr(out, "img/po.png") == NULL, "video poster not inlined");
|
||||
sf_check(strstr(out, "img/sv.png") == NULL, "svg image href not inlined");
|
||||
sf_check(strstr(out, "img/bg.png") == NULL,
|
||||
"legacy background attribute not inlined");
|
||||
sf_check(strstr(out, "img/ob.png") == NULL, "object data not inlined");
|
||||
sf_check(strstr(out, "img/em.png") == NULL, "embed src not inlined");
|
||||
sf_check(strstr(out, "img/ph.png") == NULL, "lazy placeholder not inlined");
|
||||
sf_check(strstr(out, "img/lz.png") == NULL, "data-src not inlined");
|
||||
sf_check(strstr(out, "img/lz2.png") == NULL, "data-srcset not inlined");
|
||||
sf_check(strstr(out, "img/low.png") == NULL, "lowsrc not inlined");
|
||||
/* The class gate, not the attribute name, is what keeps a page out. */
|
||||
sf_check(strstr(out, "data-src=\"other.html\"") != NULL,
|
||||
"data-src pointing at a page was inlined");
|
||||
|
||||
sf_check(strstr(out, "<a href=\"img/a%20b.png\">") != NULL, "anchor inlined");
|
||||
sf_check(strstr(out, "href=\"other.html\"") != NULL, "rel=canonical inlined");
|
||||
sf_check(strstr(out, "src=\"v.mp4\"") != NULL, "video source inlined");
|
||||
sf_check(strstr(out, "src=\"http://example.com/x.png\"") != NULL,
|
||||
"absolute URL rewritten");
|
||||
sf_check(strstr(out, "src=\"//example.com/x.png\"") != NULL,
|
||||
"site-root-relative URL rewritten");
|
||||
sf_check(sf_count(out, "QUJD") == 2, "existing data: URI not preserved");
|
||||
sf_check(strstr(out, "src=\"../escape.png\"") != NULL,
|
||||
"a reference outside the mirror was resolved");
|
||||
sf_check(strstr(out, "src=\"../img/a%20b.png\"") != NULL,
|
||||
"a leading .. was dropped instead of rejected");
|
||||
sf_check(strstr(out, "var t = \"<img src='img/a%20b.png'>\";") != NULL,
|
||||
"script body rewritten past a </scripting> lookalike");
|
||||
|
||||
/* Nothing an attribute value cannot hold: url() stays unquoted, and a quote
|
||||
that was already in the CSS is escaped. */
|
||||
sf_check(strstr(out, "url(\"data:") == NULL,
|
||||
"a quoted url() would end a style attribute");
|
||||
sf_check(strstr(out, "style=\"content:"x"; background:url(data:") !=
|
||||
NULL,
|
||||
"quote inside a rewritten style attribute not escaped");
|
||||
|
||||
sf_check(strstr(out, "img/big.png 2x") != NULL, "over-cap asset inlined");
|
||||
sf_check(strstr(out, " 1x") != NULL, "srcset descriptor lost");
|
||||
sf_check(sf_count(out, "img/a%20b.png") ==
|
||||
3, /* the anchor, the script body, and the ".." one */
|
||||
"an inlinable reference was left as a link");
|
||||
|
||||
/* The inlined stylesheet carries its own @import and url() inlined. */
|
||||
css = sf_decode(out, "text/css", NULL);
|
||||
sf_check(css != NULL, "stylesheet payload undecodable");
|
||||
if (css != NULL) {
|
||||
sf_check(strstr(css, "@import \"data:text/css;base64,") != NULL,
|
||||
"@import not inlined");
|
||||
sf_check(strstr(css, "url(data:image/png;base64,") != NULL,
|
||||
"url() in stylesheet not inlined");
|
||||
sf_check(strstr(css, "url(../img/never.png)") != NULL,
|
||||
"url() inside a CSS comment was rewritten");
|
||||
sf_check(strstr(css, "url(data:font/woff2;base64,") != NULL,
|
||||
"@font-face src not inlined");
|
||||
sf_check(strstr(css, "@import url(data:text/css;base64,") != NULL,
|
||||
"@import url() form not inlined");
|
||||
sf_check(strstr(css, "url(../img/a b.png)b.css") != NULL,
|
||||
"url() inside a string with an escaped quote was rewritten");
|
||||
/* The over-cap url() read ../img/big.png from css/; this page sits at the
|
||||
root, so it has to come back out as img/big.png or it dangles. */
|
||||
sf_check(strstr(css, "url(img/big.png)") != NULL,
|
||||
"over-cap url() not rebased onto the page's directory");
|
||||
nested = sf_decode(css, "text/css", NULL);
|
||||
sf_check(nested != NULL &&
|
||||
strstr(nested, "url(data:image/png;base64,") != NULL,
|
||||
"url() in the @import'ed stylesheet not inlined");
|
||||
freet(nested);
|
||||
freet(css);
|
||||
}
|
||||
|
||||
/* A tag with more attributes than the parser records comes back byte for
|
||||
byte, quoted '>' and all, instead of being re-scanned as markup. */
|
||||
{
|
||||
char BIGSTK wide[HTS_URLMAXSIZE * 2];
|
||||
char *before, *after;
|
||||
|
||||
fconcat(wide, sizeof(wide), root, "wide.html");
|
||||
before = readfile_utf8(wide);
|
||||
assertf(before != NULL);
|
||||
(void) singlefile_rewrite_file(opt, root, wide);
|
||||
after = readfile_utf8(wide);
|
||||
sf_check(after != NULL && strcmp(before, after) == 0,
|
||||
"an over-wide tag was rewritten");
|
||||
freet(after);
|
||||
freet(before);
|
||||
}
|
||||
|
||||
/* The same stylesheet from two directories down, where the rebase has to
|
||||
climb: the emitted path must stay inside the mirror and name the file. */
|
||||
{
|
||||
char BIGSTK deep[HTS_URLMAXSIZE * 2];
|
||||
char *dout, *dcss;
|
||||
|
||||
fconcat(deep, sizeof(deep), root, "deep/sub/page.html");
|
||||
sf_check(singlefile_rewrite_file(opt, root, deep),
|
||||
"deep page not rewritten");
|
||||
dout = readfile_utf8(deep);
|
||||
assertf(dout != NULL);
|
||||
dcss = sf_decode(dout, "text/css", NULL);
|
||||
sf_check(dcss != NULL && strstr(dcss, "url(../../img/big.png)") != NULL,
|
||||
"over-cap url() not rebased from a nested page");
|
||||
freet(dcss);
|
||||
freet(dout);
|
||||
}
|
||||
|
||||
/* Idempotence: a second pass must find nothing and leave the bytes alone. */
|
||||
sf_check(!singlefile_rewrite_file(opt, root, page), "second pass rewrote");
|
||||
{
|
||||
char *again = readfile_utf8(page);
|
||||
|
||||
sf_check(again != NULL && strcmp(again, out) == 0,
|
||||
"second pass changed the page");
|
||||
freet(again);
|
||||
}
|
||||
freet(out);
|
||||
|
||||
/* Same page, a cap above big.png: it now inlines. */
|
||||
fconcat(root, sizeof(root), argv[0], "mirror2/");
|
||||
sf_fixture(root);
|
||||
fconcat(page, sizeof(page), root, "page.html");
|
||||
opt->single_file_max_size = 1024 * 1024;
|
||||
sf_check(singlefile_rewrite_file(opt, root, page),
|
||||
"raised-cap pass changed nothing");
|
||||
out = readfile_utf8(page);
|
||||
assertf(out != NULL);
|
||||
sf_check(strstr(out, "img/big.png 2x") == NULL,
|
||||
"asset under the raised cap still a link");
|
||||
freet(out);
|
||||
|
||||
/* Nothing inlines, so the rewriter must be byte transparent. Assert on its
|
||||
output: the file it declined to write could not have changed regardless. */
|
||||
fconcat(root, sizeof(root), argv[0], "mirror3/");
|
||||
sf_fixture(root);
|
||||
fconcat(page, sizeof(page), root, "page.html");
|
||||
opt->single_file_max_size = 1;
|
||||
sf_check(!singlefile_rewrite_file(opt, root, page), "one-byte cap inlined");
|
||||
{
|
||||
String verbatim = STRING_EMPTY;
|
||||
|
||||
StringClear(verbatim);
|
||||
(void) singlefile_rewrite_html(opt, root, page, sf_page,
|
||||
sizeof(sf_page) - 1,
|
||||
SINGLEFILE_MAX_PAGE_SIZE, &verbatim);
|
||||
sf_check(StringLength(verbatim) == sizeof(sf_page) - 1 &&
|
||||
memcmp(StringBuff(verbatim), sf_page, sizeof(sf_page) - 1) ==
|
||||
0,
|
||||
"a page with nothing to inline was re-serialized differently");
|
||||
StringFree(verbatim);
|
||||
}
|
||||
(void) outlen;
|
||||
|
||||
/* The per-page budget against a self-importing stylesheet. The large-budget
|
||||
run is the control: it proves the fan-out is real, so the small one was
|
||||
cut short by the budget and not by the fixture. */
|
||||
{
|
||||
static const char bomb_css[] = "@import \"b.css\";@import \"b.css\";"
|
||||
"@import \"b.css\";@import \"b.css\";\n";
|
||||
static const char bomb_html[] = "<html><head>"
|
||||
"<link rel=\"stylesheet\" href=\"b.css\">"
|
||||
"</head></html>\n";
|
||||
const size_t css_len = sizeof(bomb_css) - 1;
|
||||
String small = STRING_EMPTY, large = STRING_EMPTY;
|
||||
|
||||
fconcat(root, sizeof(root), argv[0], "bomb/");
|
||||
sf_put(root, "b.css", bomb_css, css_len);
|
||||
fconcat(page, sizeof(page), root, "page.html");
|
||||
opt->single_file_max_size = 1024 * 1024;
|
||||
StringClear(small);
|
||||
StringClear(large);
|
||||
(void) singlefile_rewrite_html(opt, root, page, bomb_html,
|
||||
sizeof(bomb_html) - 1, (LLint) css_len * 3,
|
||||
&small);
|
||||
(void) singlefile_rewrite_html(opt, root, page, bomb_html,
|
||||
sizeof(bomb_html) - 1,
|
||||
SINGLEFILE_MAX_PAGE_SIZE, &large);
|
||||
sf_check(StringLength(large) > 4096, "the @import bomb did not fan out");
|
||||
sf_check(StringLength(small) < StringLength(large) / 8,
|
||||
"the per-page budget did not cut the fan-out short");
|
||||
/* Three files fit in three file-lengths only if the budget is charged
|
||||
before each nested rewrite; charging after measured five levels. */
|
||||
sf_check(sf_nesting(StringBuff(small), "text/css") == 3,
|
||||
"the per-page budget was not charged as each asset was taken");
|
||||
StringFree(small);
|
||||
StringFree(large);
|
||||
}
|
||||
|
||||
if (!sf_colon_ok)
|
||||
printf("singlefile: ':' is not a legal filename here, so the scheme guard "
|
||||
"only gets the weaker check\n");
|
||||
opt->single_file_max_size = saved_cap;
|
||||
printf("singlefile: %s\n", sf_err ? "FAIL" : "OK");
|
||||
return sf_err;
|
||||
}
|
||||
|
||||
// -#test=longpath <dir>: round-trip a >MAX_PATH (260) file through the file
|
||||
// wrappers, exercising hts_pathToUCS2's \\?\ prefixing on Windows (#133).
|
||||
static int st_longpath(httrackp *opt, int argc, char **argv) {
|
||||
@@ -5912,9 +5323,6 @@ static const struct selftest_entry {
|
||||
{"warc-wacz", "<dir>", "--wacz package: layout, STORE mode, sha256 digests",
|
||||
st_warc_wacz},
|
||||
#endif
|
||||
{"singlefile", "<dir>",
|
||||
"--single-file: what is inlined, the per-asset cap, idempotence",
|
||||
st_singlefile},
|
||||
};
|
||||
|
||||
static void list_selftests(void) {
|
||||
|
||||
@@ -318,18 +318,6 @@ typedef struct {
|
||||
error_redirect = "/server/error.html"; \
|
||||
} while(0)
|
||||
|
||||
/* Longest error message shown on the error page; the rest is clipped. */
|
||||
#define ERROR_MESSAGE_MAX 1024
|
||||
|
||||
/* SET_ERROR() with a printf format. Clips: these messages quote posted fields,
|
||||
whose length the client picks. */
|
||||
#define SET_ERRORF(...) \
|
||||
do { \
|
||||
char errbuf[ERROR_MESSAGE_MAX]; \
|
||||
slprintfbuff_clip(errbuf, sizeof(errbuf), __VA_ARGS__); \
|
||||
SET_ERROR(errbuf); \
|
||||
} while (0)
|
||||
|
||||
/* Longest "sid" value worth unescaping: the expected one is an md5 hex digest,
|
||||
so anything near this is already invalid and is rejected unread. */
|
||||
#define SID_VALUE_MAX 64
|
||||
@@ -737,7 +725,7 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
|
||||
fp = fopen(StringBuff(fspath), "rb");
|
||||
if (fp) {
|
||||
/* Read file */
|
||||
while (!feof(fp) && !ferror(fp)) {
|
||||
while(!feof(fp)) {
|
||||
char *str = line;
|
||||
char *pos;
|
||||
|
||||
@@ -891,18 +879,28 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
|
||||
commandEnd = 1;
|
||||
}
|
||||
} else {
|
||||
SET_ERRORF(
|
||||
"Unable to write %d bytes in the the init file %s",
|
||||
count, StringBuff(fspath));
|
||||
char tmp[1024];
|
||||
|
||||
sprintf(tmp,
|
||||
"Unable to write %d bytes in the the init file %s",
|
||||
count, StringBuff(fspath));
|
||||
SET_ERROR(tmp);
|
||||
}
|
||||
fclose(fp);
|
||||
} else {
|
||||
SET_ERRORF("Unable to create the init file %s",
|
||||
StringBuff(fspath));
|
||||
char tmp[1024];
|
||||
|
||||
sprintf(tmp, "Unable to create the init file %s",
|
||||
StringBuff(fspath));
|
||||
SET_ERROR(tmp);
|
||||
}
|
||||
} else {
|
||||
SET_ERRORF("Unable to create the directory structure in %s",
|
||||
StringBuff(fspath));
|
||||
char tmp[1024];
|
||||
|
||||
sprintf(tmp,
|
||||
"Unable to create the directory structure in %s",
|
||||
StringBuff(fspath));
|
||||
SET_ERROR(tmp);
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -980,10 +978,10 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Regular files only: reading a directory or FIFO never ends, and
|
||||
"path" may hold "..", so only the untrusted halves are checked. */
|
||||
if (fsfile[0] && strstr(file, "..") == NULL && fexist(fsfile) &&
|
||||
(fp = fopen(fsfile, "rb"))) {
|
||||
/* path itself may hold ".." (webhttrack passes "<bin>/../share"), so
|
||||
only the untrusted halves are checked: file here, website above. */
|
||||
if (fsfile[0] && strstr(file, "..") == NULL
|
||||
&& (fp = fopen(fsfile, "rb"))) {
|
||||
char ok[] =
|
||||
"HTTP/1.0 200 OK\r\n" "Connection: close\r\n"
|
||||
"Server: httrack-small-server\r\n" "Content-type: text/html\r\n"
|
||||
@@ -1042,7 +1040,7 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
|
||||
int outputmode = 0;
|
||||
|
||||
StringMemcat(headers, ok, sizeof(ok) - 1);
|
||||
while (!feof(fp) && !ferror(fp)) {
|
||||
while(!feof(fp)) {
|
||||
char *str = line;
|
||||
int prevlen = (int) StringLength(output);
|
||||
int nocr = 0;
|
||||
@@ -1476,15 +1474,14 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
|
||||
while(!feof(fp)) {
|
||||
int n = (int) fread(line, 1, sizeof(line) - 2, fp);
|
||||
|
||||
if (n <= 0) {
|
||||
break; /* short read: EOF or error, never a retry */
|
||||
if (n > 0) {
|
||||
StringMemcat(output, line, n);
|
||||
}
|
||||
StringMemcat(output, line, n);
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
} else if (strcmp(file, "/ping") == 0 ||
|
||||
strncmp(file, "/ping?", 6) == 0) {
|
||||
} else if (strcmp(file, "/ping") == 0
|
||||
|| strncmp(file, "/ping?", 6) == 0) {
|
||||
char error_hdr[] =
|
||||
"HTTP/1.0 200 Pong\r\n" "Server: httrack small server\r\n"
|
||||
"Content-type: text/html\r\n";
|
||||
|
||||
1160
src/htssinglefile.c
1160
src/htssinglefile.c
File diff suppressed because it is too large
Load Diff
@@ -1,84 +0,0 @@
|
||||
/* ------------------------------------------------------------ */
|
||||
/*
|
||||
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
|
||||
*/
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/* --single-file: post-mirror pass embedding each page's assets as data: URIs.
|
||||
Internal, not installed. Unlike -%M (one MHT archive for the whole mirror)
|
||||
it rewrites the saved pages in place and keeps page-to-page links relative,
|
||||
so the mirror stays browsable and every page also stands alone.
|
||||
Limitation: an inlined stylesheet becomes a data: URL, whose path is opaque,
|
||||
so an asset inside it that stayed a link (over the cap) no longer resolves.
|
||||
Raise --single-file-max-size past it to inline it too. */
|
||||
/* ------------------------------------------------------------ */
|
||||
|
||||
#ifndef HTS_SINGLEFILE_DEFH
|
||||
#define HTS_SINGLEFILE_DEFH
|
||||
|
||||
#include "htsopt.h"
|
||||
#include "htsstrings.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* --single-file-max-size default: a bigger asset stays an ordinary link. */
|
||||
#define SINGLEFILE_DEFAULT_MAX_SIZE (10 * 1024 * 1024)
|
||||
|
||||
/* Never base64 more than this, whatever the cap: code64() sizes in int. */
|
||||
#define SINGLEFILE_HARD_MAX_SIZE (256 * 1024 * 1024)
|
||||
|
||||
/* Total bytes one page may inline. Nested @import fans out multiplicatively,
|
||||
so a few hundred bytes of hostile CSS can otherwise ask for gigabytes. */
|
||||
#define SINGLEFILE_MAX_PAGE_SIZE (64 * 1024 * 1024)
|
||||
|
||||
/* Rewrite every HTML page the mirror produced. No-op unless opt->single_file;
|
||||
call once the tree is final, after the update purge. */
|
||||
void singlefile_process_mirror(httrackp *opt);
|
||||
|
||||
/* Rewrite one HTML document held in memory, appending the result to out.
|
||||
root is the mirror directory that references may not escape; page_path is
|
||||
the document's own path under it (both UTF-8, '/' or native separators).
|
||||
page_budget caps the total inlined bytes, since nested @import fans out
|
||||
multiplicatively; the mirror pass passes SINGLEFILE_MAX_PAGE_SIZE.
|
||||
Returns HTS_TRUE if at least one reference was replaced; out may still
|
||||
differ from the input when that is HTS_FALSE, since a style or srcset value
|
||||
is re-serialized in place. */
|
||||
hts_boolean singlefile_rewrite_html(httrackp *opt, const char *root,
|
||||
const char *page_path, const char *html,
|
||||
size_t html_len, LLint page_budget,
|
||||
String *out);
|
||||
|
||||
/* singlefile_rewrite_html() over the file at page_path, rewritten in place.
|
||||
Returns HTS_TRUE if the file changed. */
|
||||
hts_boolean singlefile_rewrite_file(httrackp *opt, const char *root,
|
||||
const char *page_path);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -1,63 +0,0 @@
|
||||
/* ------------------------------------------------------------ */
|
||||
/*
|
||||
HTTrack Website Copier, Offline Browser for Windows and Unix
|
||||
Copyright (C) 1998 Xavier Roche and other contributors
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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: in-progress display rows, shared by httrack and htsserver .h */
|
||||
/* Author: Xavier Roche */
|
||||
/* ------------------------------------------------------------ */
|
||||
|
||||
#ifndef HTS_STATS_DEFH
|
||||
#define HTS_STATS_DEFH
|
||||
|
||||
#include "htsglobal.h"
|
||||
|
||||
/* One row of the "in progress" display, shared so the CLI (httrack) and the
|
||||
web GUI (htsserver) cannot drift apart: each fills its own array. */
|
||||
|
||||
#define NStatsBuffer 14
|
||||
|
||||
#ifndef HTS_DEF_FWSTRUCT_t_StatsBuffer
|
||||
#define HTS_DEF_FWSTRUCT_t_StatsBuffer
|
||||
typedef struct t_StatsBuffer t_StatsBuffer;
|
||||
#endif
|
||||
struct t_StatsBuffer {
|
||||
char name[1024];
|
||||
char file[1024];
|
||||
char state[288]; // a short label plus back->info[256]
|
||||
char BIGSTK url_sav[HTS_URLMAXSIZE * 2]; // pour cancel
|
||||
char BIGSTK url_adr[HTS_URLMAXSIZE * 2];
|
||||
char BIGSTK url_fil[HTS_URLMAXSIZE * 2];
|
||||
LLint size;
|
||||
LLint sizetot;
|
||||
int offset;
|
||||
//
|
||||
int back;
|
||||
//
|
||||
int actived; // pour disabled
|
||||
};
|
||||
|
||||
#endif
|
||||
18
src/htsweb.h
18
src/htsweb.h
@@ -35,10 +35,26 @@ Please visit our Website: http://www.httrack.com
|
||||
|
||||
#include "htsglobal.h"
|
||||
#include "htscore.h"
|
||||
#include "htsstats.h"
|
||||
|
||||
#define NStatsBuffer 14
|
||||
#define MAX_LEN_INPROGRESS 40
|
||||
|
||||
typedef struct t_StatsBuffer {
|
||||
char name[1024];
|
||||
char file[1024];
|
||||
char state[288]; // a short label plus back->info[256]
|
||||
char url_sav[HTS_URLMAXSIZE * 2]; // pour cancel
|
||||
char url_adr[HTS_URLMAXSIZE * 2];
|
||||
char url_fil[HTS_URLMAXSIZE * 2];
|
||||
LLint size;
|
||||
LLint sizetot;
|
||||
int offset;
|
||||
//
|
||||
int back;
|
||||
//
|
||||
int actived; // pour disabled
|
||||
} t_StatsBuffer;
|
||||
|
||||
typedef struct t_InpInfo {
|
||||
int ask_refresh;
|
||||
int refresh;
|
||||
|
||||
@@ -230,7 +230,8 @@ static hts_boolean vt_size_refresh(void) {
|
||||
*/
|
||||
#define STYLE_STATVALUES VT_BOLD
|
||||
#define STYLE_STATTEXT VT_UNBOLD
|
||||
#define STYLE_STATRESET VT_UNBOLD
|
||||
#define STYLE_STATRESET VT_UNBOLD
|
||||
#define NStatsBuffer 14
|
||||
/* Rows the stats block and "Current job" take above the in-progress list. */
|
||||
#define NStatsHeaderRows 7
|
||||
|
||||
|
||||
@@ -36,7 +36,26 @@ Please visit our Website: http://www.httrack.com
|
||||
#include "htsglobal.h"
|
||||
#include "htscore.h"
|
||||
#include "htssafe.h"
|
||||
#include "htsstats.h"
|
||||
|
||||
#ifndef HTS_DEF_FWSTRUCT_t_StatsBuffer
|
||||
#define HTS_DEF_FWSTRUCT_t_StatsBuffer
|
||||
typedef struct t_StatsBuffer t_StatsBuffer;
|
||||
#endif
|
||||
struct t_StatsBuffer {
|
||||
char name[1024];
|
||||
char file[1024];
|
||||
char state[288]; // a short label plus back->info[256]
|
||||
char BIGSTK url_sav[HTS_URLMAXSIZE * 2]; // pour cancel
|
||||
char BIGSTK url_adr[HTS_URLMAXSIZE * 2];
|
||||
char BIGSTK url_fil[HTS_URLMAXSIZE * 2];
|
||||
LLint size;
|
||||
LLint sizetot;
|
||||
int offset;
|
||||
//
|
||||
int back;
|
||||
//
|
||||
int actived; // pour disabled
|
||||
};
|
||||
|
||||
#ifndef HTS_DEF_FWSTRUCT_t_InpInfo
|
||||
#define HTS_DEF_FWSTRUCT_t_InpInfo
|
||||
|
||||
@@ -131,7 +131,6 @@
|
||||
<ClCompile Include="htsproxy.c" />
|
||||
<ClCompile Include="htsrobots.c" />
|
||||
<ClCompile Include="htsselftest.c" />
|
||||
<ClCompile Include="htssinglefile.c" />
|
||||
<ClCompile Include="htssniff.c" />
|
||||
<ClCompile Include="htsthread.c" />
|
||||
<ClCompile Include="htstools.c" />
|
||||
|
||||
@@ -820,8 +820,8 @@ static PT_Element proxytrack_process_DAV_Request(PT_Indexes indexes,
|
||||
elt->size = StringLength(response);
|
||||
elt->adr = StringAcquire(&response);
|
||||
elt->statuscode = 207; /* Multi-Status */
|
||||
strcpybuff(elt->charset, "utf-8");
|
||||
strcpybuff(elt->contenttype, "text/xml");
|
||||
strcpy(elt->charset, "utf-8");
|
||||
strcpy(elt->contenttype, "text/xml");
|
||||
strcpybuff(elt->msg, "Multi-Status");
|
||||
StringFree(response);
|
||||
|
||||
@@ -877,8 +877,8 @@ static PT_Element proxytrack_process_HTTP_List(PT_Indexes indexes,
|
||||
elt->size = StringLength(html);
|
||||
elt->adr = StringAcquire(&html);
|
||||
elt->statuscode = HTTP_OK;
|
||||
strcpybuff(elt->charset, "iso-8859-1");
|
||||
strcpybuff(elt->contenttype, "text/html");
|
||||
strcpy(elt->charset, "iso-8859-1");
|
||||
strcpy(elt->contenttype, "text/html");
|
||||
strcpybuff(elt->msg, "OK");
|
||||
StringFree(html);
|
||||
return elt;
|
||||
|
||||
@@ -368,11 +368,7 @@ HTS_UNUSED static struct tm PT_GetTime(time_t t) {
|
||||
if (tm != NULL)
|
||||
return *tm;
|
||||
else {
|
||||
/* an all-zero tm has tm_mday == 0, which the ARC date field prints as a
|
||||
day of "00"; the epoch is the conventional "date unknown" */
|
||||
memset(&tmbuf, 0, sizeof(tmbuf));
|
||||
tmbuf.tm_year = 70;
|
||||
tmbuf.tm_mday = 1;
|
||||
return tmbuf;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,8 +405,8 @@ PT_Element PT_Index_HTML_BuildRootInfo(PT_Indexes indexes) {
|
||||
elt->size = StringLength(html);
|
||||
elt->adr = StringAcquire(&html);
|
||||
elt->statuscode = HTTP_OK;
|
||||
strcpybuff(elt->charset, "iso-8859-1");
|
||||
strcpybuff(elt->contenttype, "text/html");
|
||||
strcpy(elt->charset, "iso-8859-1");
|
||||
strcpy(elt->contenttype, "text/html");
|
||||
strcpybuff(elt->msg, "OK");
|
||||
StringFree(html);
|
||||
return elt;
|
||||
@@ -829,8 +829,16 @@ PT_Element PT_ElementNew(void) {
|
||||
}
|
||||
|
||||
/* ProxyTrack's htsblk_failf(): a clipped, diagnostic-only failure reason. */
|
||||
#define PT_Element_failf(R, ...) \
|
||||
slprintfbuff_clip((R)->msg, sizeof((R)->msg), __VA_ARGS__)
|
||||
static void PT_Element_failf(PT_Element r, const char *fmt, ...)
|
||||
HTS_PRINTF_FUN(2, 3);
|
||||
|
||||
static void PT_Element_failf(PT_Element r, const char *fmt, ...) {
|
||||
va_list args;
|
||||
|
||||
va_start(args, fmt);
|
||||
(void) vslprintfbuff(r->msg, sizeof(r->msg), fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
PT_Element PT_ReadCache(PT_Index index, const char *url, int flags) {
|
||||
if (index != NULL && SAFE_INDEX(index)) {
|
||||
@@ -871,11 +879,13 @@ static PT_Element PT_ReadCache__New(PT_Index index, const char *url, int flags)
|
||||
(headersSize) += (int) strlen(headers + headersSize); \
|
||||
} while(0)
|
||||
/* refvalue_size is mandatory: the cache line is bounded only by the line
|
||||
buffer, not by the destination. */
|
||||
buffer, not by the destination. Clip rather than reject, since an
|
||||
engine-written field can be wider than ours. */
|
||||
#define ZIP_READFIELD_STRING(line, value, refline, refvalue, refvalue_size) \
|
||||
do { \
|
||||
if (line[0] != '\0' && strfield2(line, refline)) { \
|
||||
(void) strclipbuff(refvalue, refvalue_size, value); \
|
||||
(refvalue)[0] = '\0'; \
|
||||
strlncatbuff(refvalue, value, refvalue_size, (refvalue_size) - 1); \
|
||||
line[0] = '\0'; \
|
||||
} \
|
||||
} while (0)
|
||||
@@ -1032,7 +1042,7 @@ static PT_Element PT_ReadCache__New_u(PT_Index index_, const char *url,
|
||||
previous_save[0] = previous_save_[0] = '\0';
|
||||
memset(r, 0, sizeof(_PT_Element));
|
||||
r->location = location_default;
|
||||
r->location[0] = '\0';
|
||||
strcpy(r->location, "");
|
||||
if (strncmp(url, "http://", 7) == 0)
|
||||
url += 7;
|
||||
hash_pos_return = coucal_read(index->hash, url, &hash_pos);
|
||||
@@ -1117,7 +1127,7 @@ static PT_Element PT_ReadCache__New_u(PT_Index index_, const char *url,
|
||||
int pathLen = (int) strlen(index->path);
|
||||
|
||||
if (pathLen > 0 && strncmp(previous_save_, index->path, pathLen) == 0) { // old (<3.40) buggy format
|
||||
strcpybuff(previous_save, previous_save_);
|
||||
strcpy(previous_save, previous_save_);
|
||||
}
|
||||
// relative ? (hack)
|
||||
else if (index->safeCache || (previous_save_[0] != '/' // /home/foo/bar.gif
|
||||
@@ -1544,7 +1554,7 @@ static int PT_LoadCache__Old(PT_Index index_, const char *filename) {
|
||||
cache->version = (int) (firstline[8] - '0'); // cache 1.x
|
||||
if (cache->version <= 5) {
|
||||
a += cache_brstr(a, firstline, sizeof(firstline));
|
||||
strcpybuff(cache->lastmodified, firstline);
|
||||
strcpy(cache->lastmodified, firstline);
|
||||
} else {
|
||||
fclose(cache->dat);
|
||||
cache->dat = NULL;
|
||||
@@ -1561,7 +1571,7 @@ static int PT_LoadCache__Old(PT_Index index_, const char *filename) {
|
||||
} else { // Vieille version du cache
|
||||
/* */
|
||||
cache->version = 0; // cache 1.0
|
||||
strcpybuff(cache->lastmodified, firstline);
|
||||
strcpy(cache->lastmodified, firstline);
|
||||
}
|
||||
|
||||
/* Create hash table for the cache (MUCH FASTER!) */
|
||||
@@ -1577,14 +1587,7 @@ static int PT_LoadCache__Old(PT_Index index_, const char *filename) {
|
||||
a++;
|
||||
/* read "host/file" */
|
||||
a += binput(a, line, HTS_URLMAXSIZE);
|
||||
{
|
||||
/* binput writes its NUL at s[max], so the second field must be
|
||||
bounded by what is left of line[], not by the same constant
|
||||
*/
|
||||
const size_t used = strlen(line);
|
||||
|
||||
a += binput(a, line + used, (int) (sizeof(line) - used - 1));
|
||||
}
|
||||
a += binput(a, line + strlen(line), HTS_URLMAXSIZE);
|
||||
/* read position */
|
||||
a += binput(a, linepos, 200);
|
||||
sscanf(linepos, "%d", &pos);
|
||||
@@ -1681,7 +1684,7 @@ static PT_Element PT_ReadCache__Old_u(PT_Index index_, const char *url,
|
||||
previous_save[0] = previous_save_[0] = '\0';
|
||||
memset(r, 0, sizeof(_PT_Element));
|
||||
r->location = location_default;
|
||||
r->location[0] = '\0';
|
||||
strcpy(r->location, "");
|
||||
if (strncmp(url, "http://", 7) == 0)
|
||||
url += 7;
|
||||
hash_pos_return = coucal_read(cache->hash, url, &hash_pos);
|
||||
@@ -1788,7 +1791,7 @@ static PT_Element PT_ReadCache__Old_u(PT_Index index_, const char *url,
|
||||
int pathLen = (int) strlen(index->path);
|
||||
|
||||
if (pathLen > 0 && strncmp(previous_save_, index->path, pathLen) == 0) { // old (<3.40) buggy format
|
||||
strcpybuff(previous_save, previous_save_);
|
||||
strcpy(previous_save, previous_save_);
|
||||
}
|
||||
// relative ? (hack)
|
||||
else if (index->safeCache || (previous_save_[0] != '/' // /home/foo/bar.gif
|
||||
@@ -2165,7 +2168,8 @@ int PT_LoadCache__Arc(PT_Index index_, const char *filename) {
|
||||
#define HTTP_READFIELD_STRING(line, value, refline, refvalue, refvalue_size) \
|
||||
do { \
|
||||
if (line[0] != '\0' && strfield2(line, refline)) { \
|
||||
(void) strclipbuff(refvalue, refvalue_size, value); \
|
||||
(refvalue)[0] = '\0'; \
|
||||
strlncatbuff(refvalue, value, refvalue_size, (refvalue_size) - 1); \
|
||||
line[0] = '\0'; \
|
||||
} \
|
||||
} while (0)
|
||||
@@ -2204,7 +2208,7 @@ static PT_Element PT_ReadCache__Arc_u(PT_Index index_, const char *url,
|
||||
location_default[0] = '\0';
|
||||
memset(r, 0, sizeof(_PT_Element));
|
||||
r->location = location_default;
|
||||
r->location[0] = '\0';
|
||||
strcpy(r->location, "");
|
||||
if (strncmp(url, "http://", 7) == 0)
|
||||
url += 7;
|
||||
hash_pos_return = coucal_read(index->hash, url, &hash_pos);
|
||||
@@ -2376,18 +2380,8 @@ static int PT_SaveCache__Arc_Fun(void *arg, const char *url, PT_Element element)
|
||||
PT_SaveCache__Arc_t *st = (PT_SaveCache__Arc_t *) arg;
|
||||
FILE *const fp = st->fp;
|
||||
struct tm *tm = convert_time_rfc822(&st->buff, element->lastmodified);
|
||||
struct tm unknown_date;
|
||||
int size_headers;
|
||||
|
||||
/* a cached entry with no parseable Last-Modified must not take the writer
|
||||
down; the epoch is the conventional "date unknown" */
|
||||
if (tm == NULL) {
|
||||
memset(&unknown_date, 0, sizeof(unknown_date));
|
||||
unknown_date.tm_year = 70;
|
||||
unknown_date.tm_mday = 1;
|
||||
tm = &unknown_date;
|
||||
}
|
||||
|
||||
sprintf(st->headers,
|
||||
"HTTP/1.0 %d %s"
|
||||
"\r\n"
|
||||
@@ -2423,7 +2417,7 @@ static int PT_SaveCache__Arc_Fun(void *arg, const char *url, PT_Element element)
|
||||
if (element->adr != NULL) {
|
||||
domd5mem(element->adr, element->size, st->md5, 1);
|
||||
} else {
|
||||
strcpybuff(st->md5, "-");
|
||||
strcpy(st->md5, "-");
|
||||
}
|
||||
fprintf(fp,
|
||||
/* nl */
|
||||
|
||||
@@ -146,13 +146,11 @@ sid=$(scrape_sid "${port}")
|
||||
resp=$(post_redirect '/foo
|
||||
X-Injected: pwned' "${port}" "${sid}") || fail "no response to a CRLF redirect value"
|
||||
stop
|
||||
# Here-string, not a pipe: grep -q SIGPIPEs the producer on the first match, and
|
||||
# pipefail then turns the hit into a silent pass.
|
||||
grep -qi 'X-Injected' <<<"${resp}" &&
|
||||
printf '%s' "${resp}" | grep -qi 'X-Injected' &&
|
||||
fail "CRLF in the redirect value reached the response"
|
||||
test "$(grep -ci '^Location:' <<<"${resp}")" -eq 0 ||
|
||||
test "$(printf '%s' "${resp}" | grep -ci '^Location:')" -eq 0 ||
|
||||
fail "CRLF redirect still emitted a Location header"
|
||||
test "$(grep -c '^HTTP/1\.' <<<"${resp}")" -eq 1 ||
|
||||
test "$(printf '%s' "${resp}" | grep -c '^HTTP/1\.')" -eq 1 ||
|
||||
fail "CRLF redirect did not yield exactly one status line"
|
||||
|
||||
# Loopback unless asked otherwise. Assert the socket, not the announcement.
|
||||
|
||||
@@ -103,10 +103,9 @@ sid=$(scrape_sid "${port}")
|
||||
test "${#sid}" -eq 32 || fail "did not scrape a 32-hex sid from the page (got '${sid}')"
|
||||
|
||||
# Accept: the legitimate flow still works. "redirect" is the cheapest field
|
||||
# with a reply that is visible in the headers. Matched from a here-string, not a
|
||||
# pipe: grep -q SIGPIPEs the producer, and pipefail then buries the verdict.
|
||||
# with a reply that is visible in the headers.
|
||||
resp=$(request "${port}" "sid=${sid}&redirect=/accepted")
|
||||
grep -q '^Location: /accepted' <<<"${resp}" ||
|
||||
printf '%s' "${resp}" | grep -q '^Location: /accepted' ||
|
||||
fail "a body carrying the correct sid was refused"
|
||||
|
||||
# Refuse: missing and wrong. A missing one is the regression under test — the
|
||||
@@ -115,12 +114,12 @@ grep -q '^Location: /accepted' <<<"${resp}" ||
|
||||
for bad in "redirect=/nosid" "sid=&redirect=/empty" \
|
||||
"sid=00000000000000000000000000000000&redirect=/wrong"; do
|
||||
resp=$(request "${port}" "${bad}")
|
||||
grep -q '^Location:' <<<"${resp}" &&
|
||||
printf '%s' "${resp}" | grep -q '^Location:' &&
|
||||
fail "body accepted without a valid sid: ${bad}"
|
||||
# A refusal has to be a well-formed reply, not a headerless fragment: the
|
||||
# release build used to emit only Content-length, which reads as a protocol
|
||||
# error to any client and hides the reason.
|
||||
grep -q '^HTTP/1\.0 403 ' <<<"${resp}" ||
|
||||
printf '%s' "${resp}" | grep -q '^HTTP/1\.0 403 ' ||
|
||||
fail "refusal was not a 403: ${bad}"
|
||||
done
|
||||
|
||||
@@ -128,23 +127,16 @@ done
|
||||
# applied to one global key store that later requests render from, and the
|
||||
# command dispatcher reads it from there. Probe the store itself — step3
|
||||
# interpolates ${projname} into its title — rather than the refused reply.
|
||||
store_page() {
|
||||
# a dead probe or a non-page reply must not read as "the store was not written"
|
||||
page=$(request "$1" "" step3) || fail "the key store probe failed"
|
||||
grep -q '^HTTP/1\.0 200 ' <<<"${page}" ||
|
||||
fail "the key store probe did not answer: '$(head -1 <<<"${page}")'"
|
||||
}
|
||||
title() { request "$1" "" step3; }
|
||||
|
||||
request "${port}" "projname=UNAUTHWRITE" >/dev/null 2>&1 || true
|
||||
store_page "${port}"
|
||||
grep -q 'UNAUTHWRITE' <<<"${page}" &&
|
||||
title "${port}" | grep -q 'UNAUTHWRITE' &&
|
||||
fail "a body without a valid sid was written to the key store"
|
||||
|
||||
# Paired accept case: without it the assertion above passes even if the server
|
||||
# simply ignores every body, which would prove nothing.
|
||||
request "${port}" "sid=${sid}&projname=AUTHWRITE" >/dev/null 2>&1 || true
|
||||
store_page "${port}"
|
||||
grep -q 'AUTHWRITE' <<<"${page}" ||
|
||||
title "${port}" | grep -q 'AUTHWRITE' ||
|
||||
fail "a body carrying the correct sid was not written to the key store"
|
||||
|
||||
echo "PASS"
|
||||
|
||||
@@ -90,15 +90,6 @@ sys.stdout.write(out.decode("latin-1"))' "$1" "$2" "$3"
|
||||
get() { request "$1" "$2" ""; }
|
||||
post() { request "$1" "" "$2"; }
|
||||
|
||||
# GET $2 into ${reply}, requiring status $3. Captured, not piped: a dead request
|
||||
# reads as marker-absent, and so do a truncated body and a 302 to the file.
|
||||
reply=
|
||||
fetch() {
|
||||
reply=$(get "$1" "$2") || fail "the request for $2 failed"
|
||||
grep -q "^HTTP/1\.0 $3 " <<<"${reply}" ||
|
||||
fail "$2: wanted a $3 reply, got '$(head -1 <<<"${reply}")'"
|
||||
}
|
||||
|
||||
url=$(start)
|
||||
port=$(portof "${url}")
|
||||
srv=$(srvpid)
|
||||
@@ -118,14 +109,13 @@ echo "LOGMARKER" >"${base}/proj/hts-log.txt"
|
||||
# with a component over NAME_MAX (mkdir refuses it whatever the uid). Must precede
|
||||
# any successful save: commandEnd then swaps the error page for the finished one.
|
||||
get "${port}" /server/style.css >/dev/null # leaves a path behind in fsfile
|
||||
saved=$(post "${port}" "sid=${sid}&command=httrack&command_do=save&winprofile=x&path=${base}/$(printf '%0300d' 0)&projname=p")
|
||||
grep -q '^Location: /server/error.html' <<<"${saved}" ||
|
||||
post "${port}" "sid=${sid}&command=httrack&command_do=save&winprofile=x&path=${base}/$(printf '%0300d' 0)&projname=p" |
|
||||
grep -q '^Location: /server/error.html' ||
|
||||
fail "the refused save did not redirect to the error page"
|
||||
|
||||
# No project yet, so no root to serve from.
|
||||
post "${port}" "sid=${sid}&projpath=${base}/" >/dev/null
|
||||
fetch "${port}" /website/secret.txt 404
|
||||
grep -q SECRETMARKER <<<"${reply}" &&
|
||||
get "${port}" /website/secret.txt | grep -q SECRETMARKER &&
|
||||
fail "a posted projpath served a file outside any project"
|
||||
|
||||
# Positive control: step4's "save settings" flow registers the project without
|
||||
@@ -136,29 +126,24 @@ body="${body}&path=${base}&projname=proj&projpath=${base}/proj/"
|
||||
post "${port}" "${body}" >/dev/null
|
||||
test -f "${base}/proj/hts-cache/winprofile.ini" ||
|
||||
fail "the project was not registered: $(cat "${log}")"
|
||||
fetch "${port}" /website/hts-log.txt 200
|
||||
grep -q LOGMARKER <<<"${reply}" ||
|
||||
get "${port}" /website/hts-log.txt | grep -q LOGMARKER ||
|
||||
fail "the registered project's mirror is not served"
|
||||
|
||||
# Same request with the root repointed: the project is legitimate, projpath is
|
||||
# not what decides where the bytes come from.
|
||||
post "${port}" "sid=${sid}&projpath=${base}/" >/dev/null
|
||||
fetch "${port}" /website/secret.txt 404
|
||||
grep -q SECRETMARKER <<<"${reply}" &&
|
||||
get "${port}" /website/secret.txt | grep -q SECRETMARKER &&
|
||||
fail "a posted projpath repointed the served root"
|
||||
post "${port}" "sid=${sid}&projpath=/etc/" >/dev/null
|
||||
fetch "${port}" /website/passwd 404
|
||||
grep -q '^root:' <<<"${reply}" &&
|
||||
get "${port}" /website/passwd | grep -q '^root:' &&
|
||||
fail "a posted projpath read an arbitrary system file"
|
||||
|
||||
# A ".." anywhere in the recorded root would escape the mirror on every later
|
||||
# request, so the save must be refused and the previous root kept.
|
||||
post "${port}" "sid=${sid}&command=httrack&command_do=save&winprofile=x&path=${base}/proj&projname=.." >/dev/null
|
||||
fetch "${port}" /website/secret.txt 404
|
||||
grep -q SECRETMARKER <<<"${reply}" &&
|
||||
get "${port}" /website/secret.txt | grep -q SECRETMARKER &&
|
||||
fail "a '..' in the saved project path escaped the mirror root"
|
||||
fetch "${port}" /website/hts-log.txt 200
|
||||
grep -q LOGMARKER <<<"${reply}" ||
|
||||
get "${port}" /website/hts-log.txt | grep -q LOGMARKER ||
|
||||
fail "rejecting the '..' root also lost the previous one"
|
||||
|
||||
# The root is now the server's own, but it is still built from two posted
|
||||
@@ -177,7 +162,7 @@ test -f "${fspath}/hts-cache/winprofile.ini" ||
|
||||
fail "the long-path project was not registered: $(cat "${log}")"
|
||||
get "${port}" "/website/${longurl}" >/dev/null 2>&1 || true
|
||||
alive "${srv}" || fail "an over-long project path crashed the server: $(cat "${log}")"
|
||||
# Not just alive: still answering.
|
||||
fetch "${port}" /server/index.html 200
|
||||
get "${port}" /server/index.html | grep -q '200 OK' ||
|
||||
fail "the server stopped answering after the over-long project path"
|
||||
|
||||
echo "PASS"
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/bin/bash
|
||||
# A cached entry whose Last-Modified is missing or unparseable must still
|
||||
# convert: the ARC writer used to dereference the parsed date unchecked.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
dir=$(mktemp -d)
|
||||
trap 'rm -rf "$dir"' EXIT
|
||||
|
||||
# $1 Last-Modified value (empty = omit the header), $2 expected archive date,
|
||||
# $3 label. The date is asserted exactly: a guard that fires unconditionally
|
||||
# would clobber the valid case, and one that skips the year/day would emit a
|
||||
# month and day of 00.
|
||||
run_case() {
|
||||
lastmod=$1
|
||||
wantdate=$2
|
||||
what=$3
|
||||
|
||||
{
|
||||
printf 'HTTP/1.1 200 OK\r\n'
|
||||
printf 'Content-Type: text/html\r\n'
|
||||
test -z "$lastmod" || printf 'Last-Modified: %s\r\n' "$lastmod"
|
||||
printf 'Content-Length: 5\r\n\r\n'
|
||||
} >"$dir/hdr"
|
||||
printf 'hello' >"$dir/body"
|
||||
alen=$(($(wc -c <"$dir/hdr") + $(wc -c <"$dir/body")))
|
||||
{
|
||||
printf 'filedesc://t.arc 0.0.0.0 20250101000000 text/plain 200 - - 0 t.arc 9\n'
|
||||
printf '2 0 test\n'
|
||||
printf '\n\n'
|
||||
printf 'http://example.com/p.html 0.0.0.0 20250101000000 text/html 200 - - 0 t.arc %d\n' "$alen"
|
||||
cat "$dir/hdr" "$dir/body"
|
||||
} >"$dir/in.arc"
|
||||
|
||||
proxytrack --convert "$dir/out.arc" "$dir/in.arc" >/dev/null 2>&1 || {
|
||||
echo "FAIL: proxytrack crashed on $what" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -aq "^http://example.com/p.html 0.0.0.0 ${wantdate} " "$dir/out.arc" || {
|
||||
echo "FAIL: $what: expected archive date ${wantdate}, got:" >&2
|
||||
grep -a '^http://example.com/' "$dir/out.arc" >&2 || echo "(entry dropped)" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# the epoch stands in for "date unknown"
|
||||
run_case '' 19700101000000 'a missing Last-Modified'
|
||||
run_case 'not a date at all' 19700101000000 'an unparseable Last-Modified'
|
||||
run_case '0' 19700101000000 'a bare 0 Last-Modified'
|
||||
# a parseable date must still come through untouched
|
||||
run_case 'Sun, 06 Nov 1994 08:49:37 GMT' 19941106084937 'a valid Last-Modified'
|
||||
@@ -1,69 +0,0 @@
|
||||
#!/bin/bash
|
||||
# A cache file whose mtime is beyond what gmtime can represent must not put a
|
||||
# day of "00" in the ARC filedesc date: PT_GetTime fell back to an all-zero tm.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "${testdir}/testlib.sh"
|
||||
|
||||
python=$(find_python) || {
|
||||
echo "python3 not found; skipping" >&2
|
||||
exit 77
|
||||
}
|
||||
|
||||
dir=$(mktemp -d)
|
||||
trap 'rm -rf "$dir"' EXIT
|
||||
|
||||
rc=0
|
||||
"$python" - "$dir/in.zip" <<'EOF' || rc=$?
|
||||
import os, sys, zipfile
|
||||
|
||||
body = b"hello world"
|
||||
meta = (
|
||||
"X-In-Cache: 1\r\n"
|
||||
"X-StatusCode: 200\r\n"
|
||||
"X-StatusMessage: OK\r\n"
|
||||
"X-Size: %d\r\n"
|
||||
"Content-Type: text/html\r\n"
|
||||
"Last-Modified: Wed, 01 Jan 2025 00:00:00 GMT\r\n"
|
||||
"X-Save: out/page.html\r\n" % len(body)
|
||||
).encode()
|
||||
|
||||
zi = zipfile.ZipInfo("example.com/page.html")
|
||||
zi.compress_type = zipfile.ZIP_STORED
|
||||
zi.extra = meta
|
||||
with zipfile.ZipFile(sys.argv[1], "w") as z:
|
||||
z.writestr(zi, body)
|
||||
|
||||
# past gmtime's range; the index timestamp is this file's mtime
|
||||
huge = 4611686018427387903
|
||||
try:
|
||||
os.utime(sys.argv[1], (huge, huge))
|
||||
except OSError:
|
||||
sys.exit(77)
|
||||
if int(os.stat(sys.argv[1]).st_mtime) < huge:
|
||||
sys.exit(77) # filesystem clamped it, nothing to test
|
||||
EOF
|
||||
test "$rc" -eq 0 || {
|
||||
test "$rc" -eq 77 || {
|
||||
echo "FAIL: could not build the cache (python exit $rc)" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "filesystem will not hold an out-of-range mtime; skipping" >&2
|
||||
exit 77
|
||||
}
|
||||
|
||||
proxytrack --convert "$dir/out.arc" "$dir/in.zip" >/dev/null 2>&1 || {
|
||||
echo "FAIL: proxytrack failed on a cache with an out-of-range mtime" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# field 3 of the filedesc line is YYYYMMDDhhmmss; the epoch stands in for
|
||||
# "unknown", and 14 digits with a real day is the point (00 is what broke)
|
||||
date=$(head -n1 "$dir/out.arc" | awk '{ print $3 }')
|
||||
test "$date" = 19700101000000 || {
|
||||
echo "FAIL: filedesc date is '$date', expected 19700101000000" >&2
|
||||
exit 1
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# The "save settings" failure messages quote a project path the request body
|
||||
# supplies, so composing them must clip instead of running off a fixed buffer.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
distdir=${top_srcdir:-$(cd "${testdir}/.." && pwd)}
|
||||
distdir=$(cd "${distdir}" && pwd)
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
command -v htsserver >/dev/null || fail "no htsserver in PATH"
|
||||
command -v python3 >/dev/null || {
|
||||
echo "python3 not found; skipping" >&2
|
||||
exit 77
|
||||
}
|
||||
|
||||
# ERROR_MESSAGE_MAX - 1 in htsserver.c.
|
||||
msgmax=1023
|
||||
|
||||
srv=
|
||||
log=$(mktemp)
|
||||
# Physical path: macOS resolves TMPDIR through /var -> private/var, and the
|
||||
# paths built below sit within a few bytes of its 1024-byte PATH_MAX.
|
||||
base=$(cd "$(mktemp -d)" && pwd -P)
|
||||
cleanup() {
|
||||
test -z "${srv}" || kill -9 "${srv}" 2>/dev/null || true
|
||||
rm -f "${log}"
|
||||
rm -rf "${base}"
|
||||
}
|
||||
trap cleanup EXIT HUP INT QUIT PIPE TERM
|
||||
|
||||
freeport() {
|
||||
python3 -c 'import socket
|
||||
s = socket.socket()
|
||||
s.bind(("127.0.0.1", 0))
|
||||
print(s.getsockname()[1])
|
||||
s.close()'
|
||||
}
|
||||
|
||||
# Echo the announced URL. Runs in a command substitution, so it is a
|
||||
# subshell and cannot export the pid: the caller reads it back with srvpid.
|
||||
start() {
|
||||
local port url
|
||||
port=$(freeport)
|
||||
: >"${log}"
|
||||
(
|
||||
trap '' TERM TTOU XFSZ
|
||||
# Caps regular-file writes, which is how the failing-write branch below
|
||||
# is reached without /dev/full; the announce rides a pipe, exempt.
|
||||
ulimit -f 8
|
||||
exec htsserver "${distdir}/" --port "${port}" 2>&1
|
||||
) | cat >"${log}" &
|
||||
for _ in $(seq 1 40); do
|
||||
url=$(sed -n 's/^URL=//p' "${log}" 2>/dev/null) && test -n "${url}" && break
|
||||
sleep 0.25
|
||||
done
|
||||
test -n "${url:-}" || fail "htsserver did not come up: $(cat "${log}")"
|
||||
echo "${url}"
|
||||
}
|
||||
|
||||
srvpid() { sed -n 's/^PID=//p' "${log}" | head -1; }
|
||||
|
||||
alive() { kill -0 "$1" 2>/dev/null; }
|
||||
|
||||
portof() { echo "${1##*:}" | tr -d /; }
|
||||
|
||||
# Raw request to 127.0.0.1:$1: GET the path $2, or POST the body $3 to / when
|
||||
# $2 is empty. Prints the reply.
|
||||
request() {
|
||||
python3 -c 'import socket, sys
|
||||
port, path, body = int(sys.argv[1]), sys.argv[2], sys.argv[3]
|
||||
if path:
|
||||
req = "GET %s HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n" % path
|
||||
else:
|
||||
req = ("POST / HTTP/1.0\r\nHost: 127.0.0.1\r\n"
|
||||
"Content-type: application/x-www-form-urlencoded\r\n"
|
||||
"Content-length: %d\r\n\r\n%s" % (len(body), body))
|
||||
s = socket.create_connection(("127.0.0.1", port), 10)
|
||||
s.settimeout(30)
|
||||
s.sendall(req.encode())
|
||||
out = b""
|
||||
while True:
|
||||
b = s.recv(65536)
|
||||
if not b:
|
||||
break
|
||||
out += b
|
||||
s.close()
|
||||
sys.stdout.write(out.decode("latin-1"))' "$1" "$2" "$3"
|
||||
}
|
||||
|
||||
get() { request "$1" "$2" ""; }
|
||||
post() { request "$1" "" "$2"; }
|
||||
|
||||
# Echo a path under the root $1 exactly $2 bytes long, in NAME_MAX-sized parts.
|
||||
padpath() {
|
||||
local root=$1 want=$2 p=$1 last
|
||||
while test $((${#p} + 203)) -lt "${want}"; do
|
||||
p="${p}/$(printf '%0200d' 0)"
|
||||
done
|
||||
last=$((want - ${#p} - 1))
|
||||
test "${last}" -ge 1 || fail "cannot build a ${want}-byte path under ${root}"
|
||||
printf '%s/%0*d' "${p}" "${last}" 0
|
||||
}
|
||||
|
||||
# Fail unless the error page renders the message opening with $1 about project
|
||||
# path $2, clipped to msgmax. error.html puts ${error} alone on its own line.
|
||||
check_clipped() {
|
||||
local prefix=$1 full=$((${#1} + ${#2})) page line shown
|
||||
test "${full}" -gt "${msgmax}" ||
|
||||
fail "a ${full}-byte message fits in ${msgmax}: it cannot show a clip"
|
||||
page=$(get "${port}" /server/error.html | tr -d '\r')
|
||||
line=$(printf '%s\n' "${page}" | grep "^${prefix}") || {
|
||||
shown=$(printf '%s\n' "${page}" | grep '^Unable to ' | cut -c1-80 || true)
|
||||
fail "the error page reports '${shown:-nothing}', want: ${prefix}"
|
||||
}
|
||||
test "${#line}" -eq "${msgmax}" ||
|
||||
fail "the message rendered ${#line} bytes, want ${full} clipped to ${msgmax}"
|
||||
}
|
||||
|
||||
url=$(start)
|
||||
port=$(portof "${url}")
|
||||
srv=$(srvpid)
|
||||
test -n "${srv}" || fail "htsserver did not report its pid"
|
||||
|
||||
# Every request body is gated by the session id (78_webhttrack-sid.test).
|
||||
sid=$(get "${port}" /server/index.html |
|
||||
sed -n 's/.*name="sid" value="\([0-9a-f]*\)".*/\1/p' | head -1)
|
||||
test "${#sid}" -eq 32 || fail "did not scrape a 32-hex sid (got '${sid}')"
|
||||
|
||||
save() {
|
||||
post "${port}" "sid=${sid}&command=httrack&command_do=save&winprofile=$2&path=$1&projname=$3"
|
||||
}
|
||||
|
||||
# A project path too long to be created at all.
|
||||
path=$(padpath "${base}/nodir" 1098)
|
||||
save "${path}" x p >/dev/null
|
||||
alive "${srv}" ||
|
||||
fail "an over-long project path crashed the server: $(cat "${log}")"
|
||||
check_clipped "Unable to create the directory structure in " "${path}/p"
|
||||
|
||||
# A creatable project path whose hts-cache is not a directory, so the init file
|
||||
# under it cannot be opened.
|
||||
path=$(padpath "${base}/isdir" 993)
|
||||
mkdir -p "${path}/p"
|
||||
ln -s /dev/null "${path}/p/hts-cache"
|
||||
save "${path}" x p >/dev/null
|
||||
alive "${srv}" || fail "an unwritable init file crashed the server: $(cat "${log}")"
|
||||
check_clipped "Unable to create the init file " "${path}/p"
|
||||
|
||||
# The init file opens, then the write fails: the profile is past both stdio's
|
||||
# buffer, so fwrite() reaches the descriptor, and the server's file-size limit.
|
||||
path=$(padpath "${base}/full" 993)
|
||||
mkdir -p "${path}/p/hts-cache"
|
||||
profile=$(printf '%01024d' 0)
|
||||
profile=${profile}${profile}${profile}${profile}
|
||||
profile=${profile}${profile}${profile}${profile}
|
||||
test "${#profile}" -eq 16384 || fail "built a ${#profile}-byte profile, want 16384"
|
||||
save "${path}" "${profile}" p >/dev/null
|
||||
alive "${srv}" || fail "a short init file write crashed the server: $(cat "${log}")"
|
||||
written=$(wc -c <"${path}/p/hts-cache/winprofile.ini" | tr -d ' ')
|
||||
test "${written}" -lt "${#profile}" ||
|
||||
fail "the file-size limit did not stop the write (${written} bytes landed)"
|
||||
check_clipped "Unable to write ${#profile} bytes in the the init file " "${path}/p"
|
||||
|
||||
get "${port}" /server/index.html | grep -q '200 OK' ||
|
||||
fail "the server stopped answering after the refused saves"
|
||||
|
||||
echo "PASS"
|
||||
@@ -1,196 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# An unchecked box posts nothing and the stored "1" survives: hence a hidden
|
||||
# companion per box, plus a disabling flag for the default-on ones.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
distdir=${top_srcdir:-$(cd "${testdir}/.." && pwd)}
|
||||
distdir=$(cd "${distdir}" && pwd)
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "${testdir}/testlib.sh"
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
command -v htsserver >/dev/null || fail "no htsserver in PATH"
|
||||
python=$(find_python) || {
|
||||
echo "python3 not found; skipping" >&2
|
||||
exit 77
|
||||
}
|
||||
|
||||
work=$(mktemp -d "${TMPDIR:-/tmp}/webhttrack_checkbox.XXXXXX") || fail "no tmpdir"
|
||||
srvlog=$(mktemp)
|
||||
srv=
|
||||
srvpid=
|
||||
cleanup() {
|
||||
# htsserver keeps SIGTERM ignored across its exec, so only -9 reaps it.
|
||||
test -z "${srvpid}" || kill -9 "${srvpid}" 2>/dev/null || true
|
||||
test -z "${srv}" || kill -9 "${srv}" 2>/dev/null || true
|
||||
wait "${srv}" 2>/dev/null || true # absorb bash's async "Killed" notice
|
||||
rm -rf "${work}" "${srvlog}"
|
||||
}
|
||||
trap cleanup EXIT HUP INT QUIT PIPE TERM
|
||||
|
||||
# An isolated HOME keeps a stray ~/.httrack.ini out of the served settings.
|
||||
sport=$("${python}" -c 'import socket
|
||||
s = socket.socket()
|
||||
s.bind(("127.0.0.1", 0))
|
||||
print(s.getsockname()[1])
|
||||
s.close()')
|
||||
(
|
||||
trap '' TERM TTOU
|
||||
export HOME="${work}"
|
||||
exec htsserver "${distdir}/" --port "${sport}" >"${srvlog}" 2>&1
|
||||
) &
|
||||
srv=$!
|
||||
for _ in $(seq 1 40); do
|
||||
url=$(sed -n 's/^URL=//p' "${srvlog}") && test -n "${url}" && break
|
||||
kill -0 "${srv}" 2>/dev/null || break
|
||||
sleep 0.25
|
||||
done
|
||||
test -n "${url:-}" || fail "htsserver did not start: $(cat "${srvlog}")"
|
||||
srvpid=$(sed -n 's/^PID=//p' "${srvlog}") # absent on Windows
|
||||
|
||||
"${python}" - "${url}" "${distdir}" <<'PY' || fail "checkbox clearing is broken (see above)"
|
||||
import glob, os, re, sys, urllib.parse, urllib.request
|
||||
|
||||
url, srcdir = sys.argv[1].rstrip("/"), sys.argv[2]
|
||||
rc = 0
|
||||
|
||||
|
||||
def check(ok, what):
|
||||
global rc
|
||||
print(("ok: " if ok else "FAIL: ") + what)
|
||||
if not ok:
|
||||
rc = 1
|
||||
|
||||
|
||||
# cache/cache2 are left to the separate rework of the dead "Cache" seed.
|
||||
skip = {"cache", "cache2"}
|
||||
page_of = {}
|
||||
boxes = 0
|
||||
for path in sorted(glob.glob(os.path.join(srcdir, "html", "server", "option*.html"))):
|
||||
page = open(path, "rb").read().decode("latin-1")
|
||||
cleared = dict((m.group(1), m.start()) for m in
|
||||
re.finditer(r'<input type="hidden" name="([^"]+)" value="">', page))
|
||||
for m in re.finditer(r'<input type="checkbox" name="([^"]+)"', page):
|
||||
boxes += 1
|
||||
if m.group(1) in skip:
|
||||
continue
|
||||
page_of[m.group(1)] = os.path.basename(path)
|
||||
at = cleared.get(m.group(1))
|
||||
check(at is not None and at < m.start(), "%s: %s is cleared before it is drawn"
|
||||
% (os.path.basename(path), m.group(1)))
|
||||
# Control: a regex that stopped matching would leave nothing to assert on.
|
||||
check(boxes >= 25, "the option pages were scanned (%d checkboxes)" % boxes)
|
||||
|
||||
sid = None
|
||||
|
||||
|
||||
def get(path):
|
||||
# The UI is served ISO-8859-1, so decode, do not assume UTF-8.
|
||||
return urllib.request.urlopen(url + path, timeout=20).read().decode("latin-1")
|
||||
|
||||
|
||||
def post(fields):
|
||||
global sid
|
||||
if sid is None:
|
||||
m = re.search(r'name="sid" value="([0-9a-f]+)"', get("/server/index.html"))
|
||||
if m is None:
|
||||
sys.exit("no session id in server/index.html")
|
||||
sid = m.group(1)
|
||||
body = "&".join("%s=%s" % (k, urllib.parse.quote(v)) for k, v in
|
||||
[("sid", sid)] + fields)
|
||||
req = urllib.request.Request(url + "/server/step4.html",
|
||||
data=body.encode("latin-1"), method="POST")
|
||||
return urllib.request.urlopen(req, timeout=20).read().decode("latin-1")
|
||||
|
||||
|
||||
def textarea(page, name):
|
||||
m = re.search(r'<textarea name="%s".*?>(.*?)</textarea>' % name, page, re.S)
|
||||
if m is None:
|
||||
sys.exit("no %s textarea in the rendered step4.html" % name)
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def checked(page, name):
|
||||
return re.search(r'name="%s"[ \t]*checked' % name, get("/server/" + page)) is not None
|
||||
|
||||
|
||||
# What each state puts on the command line (None: nothing) and its profile key.
|
||||
# Only a value-taking alias can carry a disabling flag: -I and -%I are "single"
|
||||
# (htsalias.c), so a --index=0 would read back as the bare enabling flag.
|
||||
BOXES = [
|
||||
# field profile key set cleared
|
||||
("parseall", "ParseAll", "--near", None),
|
||||
("link", "Near", "--test", None),
|
||||
("testall", "Test", "--extended-parsing", None),
|
||||
# htmlfirst emits --priority=7, which the scan-priority list also emits.
|
||||
("htmlfirst", "HTMLFirst", None, None),
|
||||
("errpage", "NoErrorPages", "--generate-errors=0", None),
|
||||
("external", "NoExternalPages", "--replace-external", None),
|
||||
("hidepwd", "NoPwdInPages", "--disable-passwords", None),
|
||||
("hidequery", "NoQueryStrings", "--include-query-string=0", None),
|
||||
("nopurge", "NoPurgeOldFiles", "--purge-old=0", None),
|
||||
("windebug", None, "--debug-headers", None),
|
||||
("ka", "KeepAlive", "--keep-alive", None),
|
||||
("remt", "RemoveTimeout", "--host-control=1", None),
|
||||
("rems", "RemoveRateout", "--host-control=2", None),
|
||||
("cookies", "Cookies", None, "--cookies=0"),
|
||||
("parsejava", "ParseJava", None, "--parse-java=0"),
|
||||
("updhack", "UpdateHack", "--updatehack", None),
|
||||
("urlhack", "URLHack", "--urlhack", None),
|
||||
("keepwww", "KeepWww", "--keep-www-prefix", None),
|
||||
("keepslashes", "KeepSlashes", "--keep-double-slashes", None),
|
||||
("keepqueryorder", "KeepQueryOrder", "--keep-query-order", None),
|
||||
("toler", "TolerantRequests", "--tolerant", None),
|
||||
("http10", "HTTP10", "--http-10", None),
|
||||
("warc", "Warc", "--warc", None),
|
||||
("norecatch", "NoRecatch", "--do-not-recatch", None),
|
||||
("logf", "Log", "--single-log", None),
|
||||
("index", "Index", None, None),
|
||||
("index2", "WordIndex", "--search-index", None),
|
||||
("ftpprox", "UseHTTPProxyForFTP", "--httpproxy-ftp", None),
|
||||
("singlefile", "SingleFile", "--single-file", None),
|
||||
]
|
||||
|
||||
for field, key, when_set, when_clear in BOXES:
|
||||
for value, want, unwanted, stored in (("on", when_set, when_clear, "1"),
|
||||
("", when_clear, when_set, "0")):
|
||||
rendered = post([(field, value)])
|
||||
cmd = textarea(rendered, "command").split()
|
||||
label = "%s %s" % (field, "set" if value else "cleared")
|
||||
if want:
|
||||
check(want in cmd, "%s emits %s" % (label, want))
|
||||
if unwanted:
|
||||
check(unwanted not in cmd, "%s drops %s" % (label, unwanted))
|
||||
if key:
|
||||
# The Windows GUI reads the same option out of the profile.
|
||||
check("%s=%s" % (key, stored) in textarea(rendered, "winprofile").splitlines(),
|
||||
"%s writes %s=%s" % (label, key, stored))
|
||||
check(checked(page_of[field], field) == bool(value),
|
||||
"%s draws %s box" % (label, "a ticked" if value else "an empty"))
|
||||
missing = sorted(set(page_of) - set(b[0] for b in BOXES))
|
||||
check(not missing, "every checkbox is exercised (missing %s)" % missing)
|
||||
|
||||
# A browser posts the companion and the ticked box under one name, so the fix
|
||||
# rests on the body loop keeping the last value; both orders pin that down.
|
||||
cmd = textarea(post([("cookies", ""), ("cookies", "on")]), "command").split()
|
||||
check("--cookies=0" not in cmd, "cookies=&cookies=on keeps cookies set")
|
||||
check(checked("option8.html", "cookies"), "cookies=&cookies=on draws a ticked box")
|
||||
cmd = textarea(post([("cookies", "on"), ("cookies", "")]), "command").split()
|
||||
check("--cookies=0" in cmd, "cookies=on&cookies= clears cookies")
|
||||
check(not checked("option8.html", "cookies"), "cookies=on&cookies= draws an empty box")
|
||||
|
||||
sys.exit(rc)
|
||||
PY
|
||||
|
||||
# A leaked htsserver wedges the parallel harness behind a green log.
|
||||
cleanup
|
||||
! kill -0 "${srv}" 2>/dev/null || fail "htsserver ${srv} survived"
|
||||
|
||||
echo "PASS"
|
||||
@@ -1,181 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# fopen() succeeds on a directory on POSIX and reading one never reaches EOF; a
|
||||
# FIFO blocks in fopen() instead. Either used to wedge the single-threaded server.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
distdir=${top_srcdir:-$(cd "${testdir}/.." && pwd)}
|
||||
distdir=$(cd "${distdir}" && pwd)
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
command -v htsserver >/dev/null || fail "no htsserver in PATH"
|
||||
command -v python3 >/dev/null || {
|
||||
echo "python3 not found; skipping" >&2
|
||||
exit 77
|
||||
}
|
||||
|
||||
srv=
|
||||
log=$(mktemp)
|
||||
base=$(mktemp -d)
|
||||
cleanup() {
|
||||
test -z "${srv}" || kill -9 "${srv}" 2>/dev/null || true
|
||||
rm -f "${log}"
|
||||
rm -rf "${base}"
|
||||
}
|
||||
trap cleanup EXIT HUP INT QUIT PIPE TERM
|
||||
|
||||
# First line only. A "| head -1" would close the pipe early and, under pipefail,
|
||||
# SIGPIPE the producer into a spurious failure.
|
||||
firstline() { echo "${1%%$'\n'*}"; }
|
||||
|
||||
freeport() {
|
||||
python3 -c 'import socket
|
||||
s = socket.socket()
|
||||
s.bind(("127.0.0.1", 0))
|
||||
print(s.getsockname()[1])
|
||||
s.close()'
|
||||
}
|
||||
|
||||
# Echo the announced URL. Runs in a command substitution, so it is a
|
||||
# subshell and cannot export the pid: the caller reads it back with srvpid.
|
||||
start() {
|
||||
local port url
|
||||
port=$(freeport)
|
||||
: >"${log}"
|
||||
(
|
||||
trap '' TERM TTOU
|
||||
exec htsserver "${distdir}/" --port "${port}" >"${log}" 2>&1
|
||||
) &
|
||||
for _ in $(seq 1 40); do
|
||||
url=$(sed -n 's/^URL=//p' "${log}" 2>/dev/null) && test -n "${url}" && break
|
||||
sleep 0.25
|
||||
done
|
||||
test -n "${url:-}" || fail "htsserver did not come up: $(cat "${log}")"
|
||||
echo "${url}"
|
||||
}
|
||||
|
||||
# The server reports its own pid; the aliveness assertion below hangs off it.
|
||||
srvpid() { firstline "$(sed -n 's/^PID=//p' "${log}")"; }
|
||||
|
||||
alive() { kill -0 "$1" 2>/dev/null; }
|
||||
|
||||
portof() { echo "${1##*:}" | tr -d /; }
|
||||
|
||||
# GET the path $2 from 127.0.0.1:$1, or POST the body $3 to / when $2 is empty.
|
||||
# The socket timeout is what makes an unfixed tree fail rather than wedge CI.
|
||||
request() {
|
||||
python3 -c 'import socket, sys
|
||||
port, path, body = int(sys.argv[1]), sys.argv[2], sys.argv[3]
|
||||
if path:
|
||||
req = "GET %s HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n" % path
|
||||
else:
|
||||
req = ("POST / HTTP/1.0\r\nHost: 127.0.0.1\r\n"
|
||||
"Content-type: application/x-www-form-urlencoded\r\n"
|
||||
"Content-length: %d\r\n\r\n%s" % (len(body), body))
|
||||
s = socket.create_connection(("127.0.0.1", port), 10)
|
||||
s.settimeout(15)
|
||||
s.sendall(req.encode())
|
||||
out = b""
|
||||
try:
|
||||
while True:
|
||||
b = s.recv(65536)
|
||||
if not b:
|
||||
break
|
||||
out += b
|
||||
except socket.timeout:
|
||||
sys.stderr.write("timed out after %d bytes\n" % len(out))
|
||||
sys.exit(9)
|
||||
s.close()
|
||||
sys.stdout.write(out.decode("latin-1"))' "$1" "$2" "$3"
|
||||
}
|
||||
|
||||
# GET $2, or fail with $3 when the reply never comes.
|
||||
get() {
|
||||
local out
|
||||
out=$(request "$1" "$2" "") || fail "$2 did not answer: $3"
|
||||
echo "${out}"
|
||||
}
|
||||
|
||||
post() { request "$1" "" "$2"; }
|
||||
|
||||
url=$(start)
|
||||
port=$(portof "${url}")
|
||||
srv=$(srvpid)
|
||||
test -n "${srv}" || fail "htsserver did not report its pid"
|
||||
|
||||
# Needs no session id and no project: html/server is a directory of the install.
|
||||
reply=$(get "${port}" /server/ "an unauthenticated request wedged the server")
|
||||
case "${reply}" in
|
||||
"HTTP/1.0 404 "*) ;;
|
||||
*) fail "a GUI directory did not answer 404: $(firstline "${reply}")" ;;
|
||||
esac
|
||||
|
||||
# Every request body is gated by the session id.
|
||||
reply=$(get "${port}" /server/index.html "the GUI is not served")
|
||||
sid=$(firstline "$(echo "${reply}" |
|
||||
sed -n 's/.*name="sid" value="\([0-9a-f]*\)".*/\1/p')")
|
||||
test "${#sid}" -eq 32 || fail "did not scrape a 32-hex sid (got '${sid}')"
|
||||
|
||||
mkdir -p "${base}/proj/sub"
|
||||
echo "PROBEMARKER" >"${base}/proj/probe.txt"
|
||||
|
||||
# mkfifo and ln -s may not work on a Windows checkout, so both stay optional.
|
||||
refuse=(/website/ /website/sub/)
|
||||
if mkfifo "${base}/proj/fifo.txt" 2>/dev/null; then
|
||||
refuse+=(/website/fifo.txt)
|
||||
fi
|
||||
ln -s probe.txt "${base}/proj/link.txt" 2>/dev/null || true
|
||||
|
||||
# step4's "save settings" flow registers the project without crawling, and that
|
||||
# is what arms the /website/ root.
|
||||
body="sid=${sid}&command=httrack&command_do=save&winprofile=x"
|
||||
body="${body}&path=${base}&projname=proj"
|
||||
post "${port}" "${body}" >/dev/null || fail "the save request did not answer"
|
||||
test -f "${base}/proj/hts-cache/winprofile.ini" ||
|
||||
fail "the project was not registered: $(cat "${log}")"
|
||||
|
||||
# Positive control: every assertion below would pass on a server serving nothing.
|
||||
served() {
|
||||
case "$(get "$1" "$2" "$3")" in
|
||||
*PROBEMARKER*) ;;
|
||||
*) fail "$3" ;;
|
||||
esac
|
||||
}
|
||||
served "${port}" /website/probe.txt "the registered project's mirror is not served"
|
||||
if test -L "${base}/proj/link.txt"; then
|
||||
# The guard stats rather than lstats, so a symlinked mirror file still serves.
|
||||
served "${port}" /website/link.txt "a symlink to a mirror file is not served"
|
||||
fi
|
||||
|
||||
# /website/ is where the GUI's own "browse mirrored site" link points.
|
||||
for path in "${refuse[@]}"; do
|
||||
reply=$(get "${port}" "${path}" "the server wedged on ${path}")
|
||||
case "${reply}" in
|
||||
"HTTP/1.0 404 "*) ;;
|
||||
*) fail "${path} did not answer 404: $(firstline "${reply}")" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# The accept loop is single-threaded: one spin denies every later request.
|
||||
alive "${srv}" || fail "the server died: $(cat "${log}")"
|
||||
served "${port}" /website/probe.txt "the server stopped answering after a directory request"
|
||||
|
||||
# This fopen() is reached before any guard, so its read loop must stop on ferror().
|
||||
mkdir -p "${base}/proj2/hts-cache/winprofile.ini"
|
||||
reply=$(post "${port}" "sid=${sid}&path=${base}&loadprojname=proj2") ||
|
||||
fail "a directory winprofile.ini wedged the server"
|
||||
case "${reply}" in
|
||||
"HTTP/1.0 3"*) ;;
|
||||
*) fail "loading a project did not redirect: $(firstline "${reply}")" ;;
|
||||
esac
|
||||
|
||||
alive "${srv}" || fail "the server died: $(cat "${log}")"
|
||||
served "${port}" /website/probe.txt "the server stopped answering after a project load"
|
||||
|
||||
echo "PASS"
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/bin/bash
|
||||
# The two halves of an .ndx entry's URL share one buffer, so the second must be
|
||||
# bounded by what the first left. The sanitizer CI build turns a regression
|
||||
# here into a hard stack-buffer-overflow.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "${testdir}/testlib.sh"
|
||||
|
||||
python=$(find_python) || {
|
||||
echo "python3 not found; skipping" >&2
|
||||
exit 77
|
||||
}
|
||||
|
||||
dir=$(mktemp -d)
|
||||
trap 'rm -rf "$dir"' EXIT
|
||||
|
||||
# each field fills binput's own bound, so together they run one past line[]
|
||||
"$python" - "$dir/foo.ndx" <<'EOF'
|
||||
import sys
|
||||
|
||||
with open(sys.argv[1], "w") as f:
|
||||
f.write("CACHE-1.4\n")
|
||||
f.write("Wed, 01 Jan 2025 00:00:00 GMT\n")
|
||||
f.write("h" * 1024 + "\n" + "f" * 1024 + "\n" + "0\n")
|
||||
# a trailing entry keeps the walk inside the buffer, so this exercises the
|
||||
# field bound alone
|
||||
f.write("tail/entry\nx\n1\n")
|
||||
EOF
|
||||
: >"$dir/foo.dat" # the reader only proceeds when the sibling .dat opens
|
||||
|
||||
proxytrack --convert "$dir/out.arc" "$dir/foo.ndx" >/dev/null 2>&1 || {
|
||||
echo "FAIL: proxytrack crashed on an .ndx with two full-length URL fields" >&2
|
||||
exit 1
|
||||
}
|
||||
@@ -1,379 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Issue #713: --single-file inlines each page's assets as data: URIs. End to
|
||||
# end over a real crawl, decoding the payloads against what the server served.
|
||||
set -e
|
||||
|
||||
: "${top_srcdir:=..}"
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "${testdir}/testlib.sh"
|
||||
server=$(nativepath "${testdir}/local-server.py")
|
||||
|
||||
python=$(find_python) || ! echo "python3 not found; skipping" >&2 || exit 77
|
||||
|
||||
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_713.XXXXXX") || exit 1
|
||||
serverpid=
|
||||
htssrv=
|
||||
cleanup() {
|
||||
stop_server "$serverpid"
|
||||
# htsserver keeps SIGTERM ignored across its exec, so only -9 reaps it.
|
||||
test -z "$htssrv" || kill -9 "$htssrv" 2>/dev/null || true
|
||||
wait "$htssrv" 2>/dev/null || true # absorb bash's async "Killed" notice
|
||||
htssrv=
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
trap cleanup EXIT HUP INT QUIT PIPE TERM
|
||||
|
||||
# --- the site ---------------------------------------------------------------
|
||||
doc="${tmpdir}/doc"
|
||||
mkdir -p "$doc"
|
||||
cat >"${doc}/index.html" <<'EOF'
|
||||
<html><head>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="canonical" href="other.html">
|
||||
</head><body>
|
||||
<img src="pixel.svg" alt="p">
|
||||
<img src="big.svg" alt="b">
|
||||
<img src="pixel.svg" data-src="lazy.svg" alt="l">
|
||||
<script src="app.js"></script>
|
||||
<a href="other.html">other</a>
|
||||
</body></html>
|
||||
EOF
|
||||
cat >"${doc}/other.html" <<'EOF'
|
||||
<html><body><a href="index.html">back</a></body></html>
|
||||
EOF
|
||||
cat >"${doc}/style.css" <<'EOF'
|
||||
@import "nested.css";
|
||||
body { background: url(pixel.svg); }
|
||||
EOF
|
||||
cat >"${doc}/nested.css" <<'EOF'
|
||||
div { background: url(pixel.svg); }
|
||||
EOF
|
||||
printf 'var app = 1;\n' >"${doc}/app.js"
|
||||
printf '<svg xmlns="http://www.w3.org/2000/svg"><rect width="1" height="1"/></svg>\n' \
|
||||
>"${doc}/pixel.svg"
|
||||
printf '<svg xmlns="http://www.w3.org/2000/svg"><circle r="1"/></svg>\n' \
|
||||
>"${doc}/lazy.svg"
|
||||
{
|
||||
printf '<svg xmlns="http://www.w3.org/2000/svg"><!--'
|
||||
head -c 20000 /dev/zero | tr '\0' 'x'
|
||||
printf '%s\n' '--><rect width="1" height="1"/></svg>'
|
||||
} >"${doc}/big.svg"
|
||||
|
||||
# --- server -----------------------------------------------------------------
|
||||
serverlog="${tmpdir}/server.log"
|
||||
: >"$serverlog"
|
||||
"$python" "$server" --root "$(nativepath "$doc")" >"$serverlog" 2>&1 &
|
||||
serverpid=$!
|
||||
port=
|
||||
for _ in $(seq 1 50); do
|
||||
line=$(head -n1 "$serverlog" 2>/dev/null)
|
||||
if test "${line%% *}" == "PORT"; then
|
||||
port="${line#PORT }"
|
||||
break
|
||||
fi
|
||||
kill -0 "$serverpid" 2>/dev/null || {
|
||||
echo "server exited early: $(cat "$serverlog")"
|
||||
exit 1
|
||||
}
|
||||
sleep 0.1
|
||||
done
|
||||
test -n "$port" || {
|
||||
echo "could not discover server port"
|
||||
exit 1
|
||||
}
|
||||
base="http://127.0.0.1:${port}"
|
||||
|
||||
which httrack >/dev/null || {
|
||||
echo "could not find httrack"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- crawl ------------------------------------------------------------------
|
||||
# The cap sits between pixel.svg and big.svg, so one inlines and one must not.
|
||||
out="${tmpdir}/crawl"
|
||||
mkdir "$out"
|
||||
common=(-O "$out" --quiet --disable-security-limits --robots=0 --timeout=30 --retries=0)
|
||||
httrack "${common[@]}" --single-file --single-file-max-size 4096 \
|
||||
"${base}/index.html" >"${tmpdir}/log1" 2>&1
|
||||
|
||||
page="${out}/127.0.0.1_${port}/index.html"
|
||||
test -f "$page" || {
|
||||
echo "FAIL: no mirrored page at $page"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- what must be inlined, byte for byte ------------------------------------
|
||||
# The decoder is python, not httrack, so a broken encoder cannot agree with
|
||||
# itself here.
|
||||
extract() {
|
||||
"$python" - "$1" "$2" "$3" <<'PY'
|
||||
import base64, re, sys
|
||||
html = open(sys.argv[1], "rb").read().decode("utf-8", "replace")
|
||||
m = re.search(r'data:%s;base64,([A-Za-z0-9+/=]*)' % re.escape(sys.argv[2]), html)
|
||||
if m is None:
|
||||
sys.exit(1)
|
||||
open(sys.argv[3], "wb").write(base64.b64decode(m.group(1)))
|
||||
PY
|
||||
}
|
||||
|
||||
printf '[image inlined and identical] ..\t'
|
||||
extract "$page" "image/svg+xml" "${tmpdir}/got.svg" || {
|
||||
echo "FAIL: no inlined image"
|
||||
exit 1
|
||||
}
|
||||
cmp -s "${doc}/pixel.svg" "${tmpdir}/got.svg" || {
|
||||
echo "FAIL: inlined image differs from the served file"
|
||||
exit 1
|
||||
}
|
||||
echo OK
|
||||
|
||||
printf '[script inlined and identical] ..\t'
|
||||
extract "$page" "application/x-javascript" "${tmpdir}/got.js" || {
|
||||
echo "FAIL: no inlined script"
|
||||
exit 1
|
||||
}
|
||||
cmp -s "${doc}/app.js" "${tmpdir}/got.js" || {
|
||||
echo "FAIL: inlined script differs from the served file"
|
||||
exit 1
|
||||
}
|
||||
echo OK
|
||||
|
||||
printf '[lazy-loaded image inlined] ..\t'
|
||||
# src is the placeholder a real page ships; the asset rides data-src.
|
||||
grep -q 'data-src="data:image/svg+xml;base64,' "$page" || {
|
||||
echo "FAIL: data-src was not inlined"
|
||||
exit 1
|
||||
}
|
||||
if grep -q 'data-src="lazy.svg"' "$page"; then
|
||||
echo "FAIL: data-src kept its link"
|
||||
exit 1
|
||||
fi
|
||||
test -f "${out}/127.0.0.1_${port}/lazy.svg" || {
|
||||
echo "FAIL: the lazy image was never downloaded, so nothing was proven"
|
||||
exit 1
|
||||
}
|
||||
echo OK
|
||||
|
||||
printf '[stylesheet inlined, recursively] ..\t'
|
||||
extract "$page" "text/css" "${tmpdir}/got.css" || {
|
||||
echo "FAIL: no inlined stylesheet"
|
||||
exit 1
|
||||
}
|
||||
grep -q 'url(data:image/svg+xml;base64,' "${tmpdir}/got.css" || {
|
||||
echo "FAIL: url() inside the inlined stylesheet was not inlined"
|
||||
exit 1
|
||||
}
|
||||
extract "${tmpdir}/got.css" "text/css" "${tmpdir}/got2.css" || {
|
||||
echo "FAIL: @import was not inlined"
|
||||
exit 1
|
||||
}
|
||||
grep -q 'url(data:image/svg+xml;base64,' "${tmpdir}/got2.css" || {
|
||||
echo "FAIL: url() inside the @import'ed stylesheet was not inlined"
|
||||
exit 1
|
||||
}
|
||||
echo OK
|
||||
|
||||
# --- what must keep its link ------------------------------------------------
|
||||
printf '[page links stay relative] ..\t'
|
||||
grep -q 'href="other.html"' "$page" || {
|
||||
echo "FAIL: the link to another page was not left relative"
|
||||
exit 1
|
||||
}
|
||||
test -f "${out}/127.0.0.1_${port}/other.html" || {
|
||||
echo "FAIL: the linked page is missing from the mirror"
|
||||
exit 1
|
||||
}
|
||||
echo OK
|
||||
|
||||
printf '[over-cap asset stays a link] ..\t'
|
||||
grep -q 'src="big.svg"' "$page" || {
|
||||
echo "FAIL: the over-cap asset did not keep its link"
|
||||
exit 1
|
||||
}
|
||||
test -f "${out}/127.0.0.1_${port}/big.svg" || {
|
||||
echo "FAIL: the over-cap asset is missing from the mirror"
|
||||
exit 1
|
||||
}
|
||||
grep -q 'over the 4096-byte cap' "${out}/hts-log.txt" || {
|
||||
echo "FAIL: the over-cap asset was not reported in the log"
|
||||
exit 1
|
||||
}
|
||||
echo OK
|
||||
|
||||
printf '[the mirror still works] ..\t'
|
||||
if grep -q 'href="style.css"' "$page"; then
|
||||
echo "FAIL: the stylesheet link survived"
|
||||
exit 1
|
||||
fi
|
||||
test -f "${out}/127.0.0.1_${port}/style.css" || {
|
||||
echo "FAIL: the mirror lost the standalone stylesheet"
|
||||
exit 1
|
||||
}
|
||||
echo OK
|
||||
|
||||
# --- a second run must not re-encode ----------------------------------------
|
||||
printf '[idempotent under --update] ..\t'
|
||||
# HTTrack stamps the time into its own footer comment, so compare without it.
|
||||
grep -v 'Mirrored from' "$page" >"${tmpdir}/page1.html"
|
||||
httrack "${common[@]}" --single-file --single-file-max-size 4096 --update \
|
||||
"${base}/index.html" >"${tmpdir}/log2" 2>&1
|
||||
grep -v 'Mirrored from' "$page" >"${tmpdir}/page2.html"
|
||||
cmp -s "${tmpdir}/page1.html" "${tmpdir}/page2.html" || {
|
||||
echo "FAIL: the second run changed the page"
|
||||
exit 1
|
||||
}
|
||||
# A double-encoded payload would decode to another data: URI, not to the SVG.
|
||||
extract "$page" "image/svg+xml" "${tmpdir}/got2.svg" || {
|
||||
echo "FAIL: no inlined image after the second run"
|
||||
exit 1
|
||||
}
|
||||
cmp -s "${doc}/pixel.svg" "${tmpdir}/got2.svg" || {
|
||||
echo "FAIL: the second run re-encoded the inlined image"
|
||||
exit 1
|
||||
}
|
||||
echo OK
|
||||
|
||||
printf '[-%%Z0 and a bad cap] ..\t'
|
||||
# -%Z0 turns it back off, so the page must come out exactly as the crawl left
|
||||
# it; a non-numeric cap is rejected and the built-in default used instead.
|
||||
off="${tmpdir}/off"
|
||||
mkdir "$off"
|
||||
httrack -O "$off" --quiet --disable-security-limits --robots=0 --timeout=30 \
|
||||
--retries=0 -%Z0 "${base}/index.html" >"${tmpdir}/log3" 2>&1
|
||||
offpage="${off}/127.0.0.1_${port}/index.html"
|
||||
if grep -q 'base64,' "$offpage"; then
|
||||
echo "FAIL: -%Z0 still inlined"
|
||||
exit 1
|
||||
fi
|
||||
bad="${tmpdir}/bad"
|
||||
mkdir "$bad"
|
||||
httrack -O "$bad" --quiet --disable-security-limits --robots=0 --timeout=30 \
|
||||
--retries=0 --single-file-max-size notanumber "${base}/index.html" \
|
||||
>"${tmpdir}/log4" 2>&1
|
||||
badpage="${bad}/127.0.0.1_${port}/index.html"
|
||||
# The default cap is 10 MB, so everything here inlines, big.svg included.
|
||||
grep -q 'data:image/svg+xml;base64,' "$badpage" || {
|
||||
echo "FAIL: a rejected cap did not fall back to the default"
|
||||
exit 1
|
||||
}
|
||||
if grep -q 'src="big.svg"' "$badpage"; then
|
||||
echo "FAIL: the default cap did not cover big.svg"
|
||||
exit 1
|
||||
fi
|
||||
echo OK
|
||||
|
||||
printf '[the rewritten page keeps the mirror file mode] ..\t'
|
||||
# The rewrite spools to a temp and renames, bypassing the engine's chmod.
|
||||
if is_windows; then
|
||||
echo "SKIP (no POSIX file modes)"
|
||||
else
|
||||
umaskdir="${tmpdir}/umask"
|
||||
mkdir "$umaskdir"
|
||||
(
|
||||
umask 077
|
||||
httrack -O "$umaskdir" --quiet --disable-security-limits --robots=0 \
|
||||
--timeout=30 --retries=0 --single-file "${base}/index.html" \
|
||||
>"${tmpdir}/log5" 2>&1
|
||||
)
|
||||
umaskroot="${umaskdir}/127.0.0.1_${port}"
|
||||
mode() { "$python" -c 'import os,sys
|
||||
print("%04o" % (os.stat(sys.argv[1]).st_mode & 0o777))' "$1"; }
|
||||
pagemode=$(mode "${umaskroot}/index.html")
|
||||
assetmode=$(mode "${umaskroot}/big.svg")
|
||||
test "$pagemode" = "$assetmode" || {
|
||||
echo "FAIL: page is $pagemode, the asset beside it is $assetmode"
|
||||
exit 1
|
||||
}
|
||||
echo OK
|
||||
fi
|
||||
|
||||
# --- the rewriter in isolation, over a hand-built mirror --------------------
|
||||
# After the crawl assertions, so a failure here cannot hide them: the two cover
|
||||
# different ground (this one reaches media, anchors and the mirror-root clamp).
|
||||
printf '[engine self-test] ..\t'
|
||||
st=$(httrack -O /dev/null "-#test=singlefile" "${tmpdir}/st/" 2>/dev/null)
|
||||
case "$st" in
|
||||
*": OK") echo OK ;;
|
||||
*)
|
||||
echo "FAIL: $st"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- the web GUI must emit the same options ---------------------------------
|
||||
distdir=$(cd "${top_srcdir}" && pwd)
|
||||
printf '[project reload maps the keys back] ..\t'
|
||||
grep -q 'do:copy:SingleFile:singlefile' "${distdir}/html/server/step2.html" || {
|
||||
echo "FAIL: step2.html does not restore SingleFile"
|
||||
exit 1
|
||||
}
|
||||
grep -q 'do:copy:SingleFileMaxSize:singlefilemax' "${distdir}/html/server/step2.html" || {
|
||||
echo "FAIL: step2.html does not restore SingleFileMaxSize"
|
||||
exit 1
|
||||
}
|
||||
echo OK
|
||||
|
||||
printf '[webhttrack renders the options] ..\t'
|
||||
# The Windows job builds httrack.exe only and reaches this file through its
|
||||
# *_local-*.test glob, so report a skip there rather than a pass.
|
||||
if ! command -v htsserver >/dev/null; then
|
||||
echo "SKIP (no htsserver in PATH)"
|
||||
exit 77
|
||||
fi
|
||||
sport=$("$python" -c 'import socket
|
||||
s = socket.socket()
|
||||
s.bind(("127.0.0.1", 0))
|
||||
print(s.getsockname()[1])
|
||||
s.close()')
|
||||
srvlog="${tmpdir}/htsserver.log"
|
||||
(
|
||||
trap '' TERM TTOU
|
||||
export HOME="${tmpdir}/home"
|
||||
mkdir -p "$HOME"
|
||||
exec htsserver "${distdir}/" --port "${sport}" >"${srvlog}" 2>&1
|
||||
) &
|
||||
htssrv=$!
|
||||
url=
|
||||
for _ in $(seq 1 40); do
|
||||
url=$(sed -n 's/^URL=//p' "$srvlog" 2>/dev/null) && test -n "$url" && break
|
||||
kill -0 "$htssrv" 2>/dev/null || break
|
||||
sleep 0.25
|
||||
done
|
||||
test -n "${url:-}" || {
|
||||
echo "FAIL: htsserver did not start: $(cat "$srvlog")"
|
||||
exit 1
|
||||
}
|
||||
"$python" - "$url" <<'PY' || exit 1
|
||||
import re, sys, urllib.parse, urllib.request
|
||||
|
||||
url = sys.argv[1]
|
||||
form = urllib.request.urlopen(url + "server/index.html", timeout=20).read()
|
||||
m = re.search(rb'name="sid" value="([0-9a-f]+)"', form)
|
||||
if not m:
|
||||
sys.exit("FAIL: no session id in server/index.html")
|
||||
fields = [("sid", m.group(1).decode()), ("singlefile", "on"),
|
||||
("singlefilemax", "5000")]
|
||||
body = "&".join("%s=%s" % (k, urllib.parse.quote(v)) for k, v in fields)
|
||||
req = urllib.request.Request(url + "server/step4.html",
|
||||
data=body.encode("latin-1"), method="POST")
|
||||
page = urllib.request.urlopen(req, timeout=20).read().decode("latin-1")
|
||||
|
||||
|
||||
def textarea(name):
|
||||
m = re.search(r'<textarea name="%s".*?>(.*?)</textarea>' % name, page, re.S)
|
||||
if m is None:
|
||||
sys.exit("FAIL: no %s textarea in the rendered step4.html" % name)
|
||||
return m.group(1)
|
||||
|
||||
|
||||
cmd = textarea("command")
|
||||
for want in ("--single-file", "--single-file-max-size=5000"):
|
||||
if want not in cmd:
|
||||
sys.exit("FAIL: %s missing from the command line\n%s" % (want, cmd))
|
||||
ini = textarea("winprofile").replace("\r\n", "\n")
|
||||
for want in ("\nSingleFile=1\n", "\nSingleFileMaxSize=5000\n"):
|
||||
if want not in ini:
|
||||
sys.exit("FAIL: %r missing from winprofile.ini\n%s" % (want, ini))
|
||||
PY
|
||||
echo OK
|
||||
@@ -183,13 +183,6 @@ TESTS = \
|
||||
83_webhttrack-argescape.test \
|
||||
84_webhttrack-mirror-verbatim.test \
|
||||
85_webhttrack-projpath.test \
|
||||
86_local-proxytrack-cache-longfields.test \
|
||||
87_local-proxytrack-nodate.test \
|
||||
88_local-proxytrack-badmtime.test \
|
||||
89_webhttrack-error-overflow.test \
|
||||
90_webhttrack-checkbox-clear.test \
|
||||
91_webhttrack-directory.test \
|
||||
92_local-proxytrack-ndx-fields.test \
|
||||
93_local-single-file.test
|
||||
86_local-proxytrack-cache-longfields.test
|
||||
|
||||
CLEANFILES = check-network_sh.cache
|
||||
|
||||
@@ -46,15 +46,11 @@ cat >"$stubdir/x-www-browser" <<EOF
|
||||
echo "stub browser invoked with: \$1" >&2
|
||||
# Also fetch an option page and require a rendered title='' tooltip: proves the
|
||||
# option template expands and the \${html:} filter escapes into the attribute.
|
||||
# option9 additionally proves the WARC control renders with its expanded label,
|
||||
# and option2 the --single-file pair: on field names plus the absence of an
|
||||
# unexpanded key, since the default locale here is French.
|
||||
# option9 additionally proves the WARC control renders with its expanded label.
|
||||
opturl="\${1%/}/server/option2.html"
|
||||
warcurl="\${1%/}/server/option9.html"
|
||||
if body="\$(curl -fsSL --max-time 20 "\$1")" && printf '%s' "\$body" | grep -qai httrack && printf '%s' "\$body" | grep -qaF step2.html &&
|
||||
opt="\$(curl -fsSL --max-time 20 "\$opturl")" && printf '%s' "\$opt" | grep -qaF "title='" &&
|
||||
printf '%s' "\$opt" | grep -qaF 'name="singlefile"' && printf '%s' "\$opt" | grep -qaF 'name="singlefilemax"' &&
|
||||
! printf '%s' "\$opt" | grep -qaF '\${LANG_SINGLEFILE}' &&
|
||||
warc="\$(curl -fsSL --max-time 20 "\$warcurl")" && printf '%s' "\$warc" | grep -qaF 'name="warcfile"' && printf '%s' "\$warc" | grep -qaF WARC; then
|
||||
echo PASS >"$marker"
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user