mirror of
https://github.com/xroche/httrack.git
synced 2026-07-27 11:03:13 +03:00
Compare commits
8 Commits
fix/refetc
...
singlefile
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
245159b5c1 | ||
|
|
fa2d5d7bc2 | ||
|
|
74280472bb | ||
|
|
36b9be497c | ||
|
|
1300d297aa | ||
|
|
039ff23ff8 | ||
|
|
072f5e6528 | ||
|
|
de8c0eebfc |
@@ -173,7 +173,10 @@ AC_CHECK_TYPE(sa_family_t, [], [AC_DEFINE([sa_family_t], [uint16_t], [sa_family_
|
||||
AX_CHECK_ALIGNED_ACCESS_REQUIRED
|
||||
|
||||
# check for various headers
|
||||
AC_CHECK_HEADERS([execinfo.h sys/ioctl.h])
|
||||
AC_CHECK_HEADERS([execinfo.h sys/ioctl.h sys/random.h])
|
||||
|
||||
## CSPRNG for the --single-file mark; /dev/urandom is the fallback
|
||||
AC_CHECK_FUNCS([getrandom])
|
||||
|
||||
### zlib (mandatory)
|
||||
CHECK_ZLIB()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<img src="a.png">
|
||||
<img src=big.png alt=over-cap>
|
||||
<img src="../escape.png"><img src="/abs.png"><img src="data:,x">
|
||||
<img src="a.png#!htsinline">
|
||||
<img src=big.png#!htsinline alt=over-cap>
|
||||
<img src="../escape.png#!htsinline"><img src="/abs.png#!htsinline">
|
||||
<img src="data:,x#!htsinline">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<link rel="stylesheet" href="s.css">
|
||||
<link rel=icon href=a.png>
|
||||
<link rel="next" href="p2.html">
|
||||
<link rel="preload" href="j.js">
|
||||
<link rel="stylesheet" href="s.css#!htsinline">
|
||||
<link rel="icon" href="a.png#!htsinline">
|
||||
<link rel="canonical" href="other.html#!htsinline">
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
<img src="unterminated.png
|
||||
<div style="background:url(a.png">
|
||||
<style>@import url(
|
||||
<!-- unterminated comment
|
||||
<a href=
|
||||
<p a=1 b=2 c=3 d=4 e=5 f=6 g=7 h=8 i=9 j=10 title="> <img src=a.png#!htsinline>">x</p>
|
||||
<!--><img src="a.png#!htsinline">
|
||||
<img src="a.png#!htsinline" >
|
||||
|
||||
@@ -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
fuzz/corpus/singlefile/mark-at-eof.html
Normal file
1
fuzz/corpus/singlefile/mark-at-eof.html
Normal file
@@ -0,0 +1 @@
|
||||
a.png#!htsinline
|
||||
5
fuzz/corpus/singlefile/mark-degenerate.html
Normal file
5
fuzz/corpus/singlefile/mark-degenerate.html
Normal file
@@ -0,0 +1,5 @@
|
||||
#!htsinline#!htsinline#!htsinline
|
||||
=#!htsinline=
|
||||
"#!htsinline"
|
||||
a.png#!htsinlin
|
||||
#!htsinlin
|
||||
1
fuzz/corpus/singlefile/mark-only.html
Normal file
1
fuzz/corpus/singlefile/mark-only.html
Normal file
@@ -0,0 +1 @@
|
||||
#!htsinline
|
||||
@@ -1,4 +1,3 @@
|
||||
<script>var s="</scripting>"; if(a</b) x=1;</script>
|
||||
<script src="j.js"></script>
|
||||
<textarea></textareas></textarea>
|
||||
<title></titles></title>
|
||||
<script src="j.js#!htsinline"></script>
|
||||
<script>var u="a.png#!htsinline";</script>
|
||||
<textarea>a.png#!htsinline</textarea>
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
<img srcset="a.png 1x, big.png 2x, a.png 100w">
|
||||
<source srcset="a.png,, a.png 2x," src="a.png">
|
||||
<img srcset="a.png#!htsinline 1x, big.png#!htsinline 2x, a.png#!htsinline 480w">
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
<div style="background:url(a.png);list-style:url('a.png')"></div>
|
||||
<p style='background:url("a.png")'>x</p>
|
||||
<div style="background:url(a.png#!htsinline)"></div>
|
||||
<div style='background:url("a.png#!htsinline")'></div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<style>@import "s.css";
|
||||
@import url(sub/b.css);
|
||||
div{background:url(a.png)}
|
||||
/* url(a.png) */ p:after{content:"url(a.png)"}
|
||||
<style>@import "s.css#!htsinline";
|
||||
@import url(sub/b.css#!htsinline);
|
||||
div{background:url(a.png#!htsinline)}
|
||||
</style>
|
||||
|
||||
4
fuzz/corpus/singlefile/traversal.html
Normal file
4
fuzz/corpus/singlefile/traversal.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<img src="../../../../etc/passwd#!htsinline">
|
||||
<img src="a.png#!htsinline#frag?q=1">
|
||||
<img src="sub/../a.png#!htsinline">
|
||||
<img src="./a.png#!htsinline">
|
||||
@@ -95,10 +95,13 @@ static void sf_init(void) {
|
||||
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");
|
||||
/* Marked, so an inlined stylesheet recurses into its own marks and its
|
||||
un-inlinable reference is rebased; unmarked assets leave the target as a
|
||||
bare scan that reaches nothing. */
|
||||
sf_text("s.css", "@import url(sub/b.css" SINGLEFILE_MARK ");\n"
|
||||
"div{background:url(a.png" SINGLEFILE_MARK ")}\n"
|
||||
"p{background:url(big.png" SINGLEFILE_MARK ")}\n");
|
||||
sf_text("sub/b.css", "p{background:url(../a.png" SINGLEFILE_MARK ")}\n");
|
||||
}
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
|
||||
@@ -585,14 +585,6 @@ static int create_back_tmpfile(httrackp *opt, lien_back *const back,
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Move src onto dst; RENAME does not clobber an existing target on Windows. */
|
||||
static hts_boolean replace_file(const char *src, const char *dst) {
|
||||
if (RENAME(src, dst) == 0)
|
||||
return HTS_TRUE;
|
||||
(void) UNLINK(dst);
|
||||
return RENAME(src, dst) == 0 ? HTS_TRUE : HTS_FALSE;
|
||||
}
|
||||
|
||||
/* Commit or restore a re-fetch backup (#77 follow-up): a re-fetch over an
|
||||
existing file moved the good copy to back->tmpfile before truncating url_sav.
|
||||
commit keeps the new file and drops the backup; else restore it so an aborted
|
||||
@@ -610,7 +602,7 @@ static void back_finalize_backup(httrackp *opt, lien_back *const back,
|
||||
}
|
||||
/* On failure keep the backup: an orphaned temp beats losing the good copy.
|
||||
*/
|
||||
if (!replace_file(back->tmpfile, back->url_sav))
|
||||
if (!hts_rename_over(back->tmpfile, back->url_sav))
|
||||
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
|
||||
"could not restore %s; previous copy kept as %s",
|
||||
back->url_sav, back->tmpfile);
|
||||
@@ -741,7 +733,7 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
|
||||
"Read error when decompressing");
|
||||
}
|
||||
UNLINK(unpacked);
|
||||
} else if (replace_file(unpacked, back[p].url_sav)) {
|
||||
} else if (hts_rename_over(unpacked, back[p].url_sav)) {
|
||||
/* The temp bypassed filecreate(), which is what chmods. */
|
||||
#ifndef _WIN32
|
||||
chmod(back[p].url_sav, HTS_ACCESS_FILE);
|
||||
|
||||
@@ -1221,6 +1221,16 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
|
||||
opt->mimehtml = 0;
|
||||
com++;
|
||||
}
|
||||
if (opt->mimehtml && opt->single_file) {
|
||||
HTS_PANIC_PRINTF(
|
||||
"-%M and --single-file are two ways to make one "
|
||||
"self-contained file, so pick one: MIME (-%M) carries "
|
||||
"text parts without the base64 tax and stores a shared "
|
||||
"asset once; single-file HTML opens anywhere by "
|
||||
"double-click.");
|
||||
htsmain_free();
|
||||
return -1;
|
||||
}
|
||||
break;
|
||||
case 'k':
|
||||
opt->nokeepalive = 0;
|
||||
@@ -1832,6 +1842,16 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
|
||||
com++;
|
||||
}
|
||||
}
|
||||
if (opt->single_file && opt->mimehtml) {
|
||||
HTS_PANIC_PRINTF(
|
||||
"-%M and --single-file are two ways to make one "
|
||||
"self-contained file, so pick one: MIME (-%M) carries "
|
||||
"text parts without the base64 tax and stores a shared "
|
||||
"asset once; single-file HTML opens anywhere by "
|
||||
"double-click.");
|
||||
htsmain_free();
|
||||
return -1;
|
||||
}
|
||||
break;
|
||||
case 'm': // sitemap / sitemap-url: seed the crawl from sitemaps
|
||||
if (*(com + 1) == 'u') { // --sitemap-url URL: explicit sitemap
|
||||
|
||||
@@ -6037,6 +6037,7 @@ HTSEXT_API httrackp *hts_create_opt(void) {
|
||||
opt->changes_state = NULL;
|
||||
opt->single_file = HTS_FALSE;
|
||||
opt->single_file_max_size = SINGLEFILE_DEFAULT_MAX_SIZE;
|
||||
opt->singlefile_state = NULL;
|
||||
StringCopy(opt->why_url, "");
|
||||
opt->pause_min_ms = 0;
|
||||
opt->pause_max_ms = 0;
|
||||
@@ -6191,6 +6192,7 @@ HTSEXT_API void hts_free_opt(httrackp * opt) {
|
||||
StringFree(opt->warc_file);
|
||||
StringFree(opt->sitemap_url);
|
||||
hts_sitemap_free(opt); /* backstop: httpmirror's early-return paths */
|
||||
singlefile_free(opt);
|
||||
|
||||
hts_changes_free_opt(opt);
|
||||
|
||||
|
||||
@@ -564,6 +564,8 @@ struct httrackp {
|
||||
rather than in htsoptstate because that struct is embedded by value, so
|
||||
growing it would shift every httrackp field declared after it. */
|
||||
void *sitemap_state; /**< hts_sitemap_state*, or NULL. Tail: ABI */
|
||||
void *singlefile_state; /**< hts_singlefile_state*, or NULL; holds this run's
|
||||
mark secret, never persisted. Tail: ABI */
|
||||
};
|
||||
|
||||
/* Running statistics for a mirror. */
|
||||
|
||||
@@ -59,6 +59,7 @@ Please visit our Website: http://www.httrack.com
|
||||
|
||||
// parser
|
||||
#include "htsparse.h"
|
||||
#include "htssinglefile.h"
|
||||
#include "htsback.h"
|
||||
|
||||
// arrays
|
||||
@@ -384,6 +385,7 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
FILE *fp = NULL; // fichier écrit localement
|
||||
const char *html = r->adr; // pointeur (on parcours)
|
||||
const char *lastsaved; // adresse du dernier octet sauvé + 1
|
||||
hts_boolean sf_may_mark = HTS_FALSE; // --single-file: mark this one?
|
||||
|
||||
hts_log_print(opt, LOG_DEBUG, "scanning file %s%s (%s)..", urladr(), urlfil(),
|
||||
savename());
|
||||
@@ -399,6 +401,13 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
r->adr[i] = ' ';
|
||||
}
|
||||
}
|
||||
/* --single-file: RFC 2046 makes the generator responsible for a
|
||||
boundary that cannot collide with what it encapsulates. Same duty
|
||||
here, and the same negligible odds: only chance can put this run's
|
||||
secret in a fetched document, since no site can spell it. */
|
||||
if (opt->single_file) {
|
||||
sf_may_mark = singlefile_may_mark(opt, r->adr, (size_t) r->size);
|
||||
}
|
||||
}
|
||||
|
||||
// Indexing!
|
||||
@@ -3043,10 +3052,32 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
|
||||
if ((opt->getmode & HTS_GETMODE_HTML) && (ptr > 0)) {
|
||||
|
||||
/* --single-file: tag the reference for the
|
||||
end-of-mirror pass. A fragment, so a mirror left
|
||||
marked by an interrupted run still browses. */
|
||||
const char sf_class =
|
||||
(opt->single_file && sf_may_mark && !in_media &&
|
||||
p_type == 0 && !p_searchMETAURL)
|
||||
? singlefile_ref_class(
|
||||
intag_start_valid ? intag_name : NULL,
|
||||
tag_attr_start)
|
||||
: 0;
|
||||
const size_t sf_start = TypedArraySize(output_buffer);
|
||||
|
||||
// écrire le lien modifié, relatif
|
||||
// Note: escape all chars, even >127 (no UTF)
|
||||
HT_ADD_HTMLESCAPED_FULL(tempo);
|
||||
|
||||
/* Measured on what was appended, not on tempo: the
|
||||
escape above is free to change the length. */
|
||||
if (sf_class != 0) {
|
||||
char sf_mark[SINGLEFILE_MARK_MAX];
|
||||
|
||||
HT_ADD(singlefile_mark(
|
||||
opt, sf_mark, sizeof(sf_mark), sf_class,
|
||||
TypedArraySize(output_buffer) - sf_start));
|
||||
}
|
||||
|
||||
// Add query-string, for informational purpose only
|
||||
// Useless, because all parameters-pages are saved into different targets
|
||||
if (opt->includequery) {
|
||||
|
||||
@@ -5201,11 +5201,47 @@ static hts_boolean sf_try_put(const char *dir, const char *rel,
|
||||
return HTS_TRUE;
|
||||
}
|
||||
|
||||
/* Expand a fixture: \001<ref>\002 becomes <ref> plus this run's mark for it.
|
||||
The secret is random per run, so a fixture cannot spell a mark itself. */
|
||||
static void sf_expand_fixture(httrackp *opt, const char *in, size_t len,
|
||||
String *out) {
|
||||
size_t i, start = 0;
|
||||
|
||||
StringClear(*out);
|
||||
for (i = 0; i < len; i++) {
|
||||
if (in[i] == '\001') {
|
||||
start = StringLength(*out);
|
||||
} else if (in[i] == '\002') {
|
||||
char mark[SINGLEFILE_MARK_MAX];
|
||||
|
||||
StringCat(*out,
|
||||
singlefile_mark(opt, mark, sizeof(mark), SINGLEFILE_CLASS_ANY,
|
||||
StringLength(*out) - start));
|
||||
} else {
|
||||
StringAddchar(*out, in[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void sf_put(const char *dir, const char *rel, const void *data,
|
||||
size_t len) {
|
||||
assertf(sf_try_put(dir, rel, data, len));
|
||||
}
|
||||
|
||||
/* sf_put() for a text fixture, expanding its \001<ref>\002 delimiters. Binary
|
||||
fixtures must not go through here: sf_png carries those very bytes. */
|
||||
static size_t sf_put_marked(httrackp *opt, const char *dir, const char *rel,
|
||||
const void *data, size_t len) {
|
||||
String body = STRING_EMPTY;
|
||||
size_t written;
|
||||
|
||||
sf_expand_fixture(opt, (const char *) data, len, &body);
|
||||
assertf(sf_try_put(dir, rel, StringBuff(body), StringLength(body)));
|
||||
written = StringLength(body);
|
||||
StringFree(body);
|
||||
return written;
|
||||
}
|
||||
|
||||
/* Number of times needle occurs in hay. */
|
||||
static int sf_count(const char *hay, const char *needle) {
|
||||
const size_t l = strlen(needle);
|
||||
@@ -5315,75 +5351,80 @@ static const char sf_png[] = "\x89PNG\r\n\x1a\n\x00\x01\x02\xff";
|
||||
|
||||
static const char sf_page[] =
|
||||
"<html><head>\n"
|
||||
"<link rel=\"stylesheet\" href=\"css/main.css\">\n"
|
||||
"<link rel=\"canonical\" href=\"other.html\">\n"
|
||||
"<link rel=\"stylesheet\" href=\"\001css/main.css\002\">\n"
|
||||
"<link rel=\"canonical\" href=\"\001other.html\002\">\n"
|
||||
"<title>t</title>\n"
|
||||
"<style>body { background: url(\"img/a%20b.png\"); }</style>\n"
|
||||
"<style>body { background: url(\"\001img/a%20b.png\002\"); }</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=\"\001img/a%20b.png\002\" srcset=\"\001img/a%20b.png\002 "
|
||||
"1x, \001img/big.png\002 2x\">\n"
|
||||
"<link rel=\"icon\" href=\"\001icon.png\002\">\n"
|
||||
"<link rel=\"preload\" as=\"font\" href=\"\001font/f.woff2\002\">\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"
|
||||
"<input type=\"image\" src=\"\001img/in.png\002\">\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"
|
||||
"<img src=\"\001img/ph.png\002\" data-src=\"\001img/lz.png\002\" "
|
||||
"data-srcset=\"\001img/lz2.png\002 2x\" "
|
||||
"lowsrc=\"\001img/low.png\002\">\n"
|
||||
"<object data=\"\001img/ob.png\002\"></object>\n"
|
||||
"<embed src=\"\001img/em.png\002\">\n"
|
||||
"<img data-src=\"\001other.html\002\">\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"
|
||||
"<video poster=\"\001img/po.png\002\" controls>"
|
||||
"<source src=\"\001v.mp4\002\" type=\"video/mp4\"></video>\n"
|
||||
"<svg><image href=\"\001img/sv.png\002\"/></svg>\n"
|
||||
"<table background=\"\001img/bg.png\002\"><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"
|
||||
"<img src=\"\001../escape.png\002\">\n"
|
||||
"<img src=\"\001../img/a%20b.png\002\">\n"
|
||||
"<a href=\"img/a%20b.png\">link</a>\n"
|
||||
"<script src=\"js/app.js\"></script>\n"
|
||||
"<script src=\"\001js/app.js\002\"></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"
|
||||
"<img src=\"\001missing.png\002\" >\n"
|
||||
"<!--><img src=\"\001img/a%20b.png\002\">\n"
|
||||
"<div style=\"background:url(\001img/a%20b.png\002)\"></div>\n"
|
||||
"<div style='content:\"x\"; "
|
||||
"background:url(\001img/a%20b.png\002)'></div>\n"
|
||||
"</body></html>\n";
|
||||
|
||||
/* Lay a small mirror down under root. */
|
||||
static void sf_fixture(const char *root) {
|
||||
static void sf_fixture(httrackp *opt, 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 \"\001sub/nested.css\002\";\n"
|
||||
"@import url(\"\001sub/two.css\002\");\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"
|
||||
"@font-face { font-family: f; src: url(\001../font/f.woff2\002); }\n"
|
||||
"body { background: url(\001../img/a%20b.png\002); }\n"
|
||||
"div { background: url(\001../img/big.png\002); }\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 nested[] =
|
||||
"div { background: url(\001../../img/a%20b.png\002); }\n";
|
||||
static const char two[] =
|
||||
"p { background: url(\001../../img/a%20b.png\002); }\n";
|
||||
static const char deep[] = "<html><head><link rel=\"stylesheet\" "
|
||||
"href=\"\001../../css/main.css\002\">\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_marked(opt, root, "page.html", sf_page, sizeof(sf_page) - 1);
|
||||
sf_put_marked(opt, 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_marked(opt, root, "css/main.css", css, sizeof(css) - 1);
|
||||
sf_put_marked(opt, root, "css/sub/nested.css", nested, sizeof(nested) - 1);
|
||||
sf_put_marked(opt, 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));
|
||||
@@ -5421,7 +5462,7 @@ static void sf_fixture(const char *root) {
|
||||
}
|
||||
StringCat(wide,
|
||||
" title=\"> <img src=img/a%20b.png> \">end</p></body></html>");
|
||||
sf_put(root, "wide.html", StringBuff(wide), StringLength(wide));
|
||||
sf_put_marked(opt, root, "wide.html", StringBuff(wide), StringLength(wide));
|
||||
StringFree(wide);
|
||||
}
|
||||
sf_put(root, "v.mp4",
|
||||
@@ -5447,7 +5488,7 @@ static int st_singlefile(httrackp *opt, int argc, char **argv) {
|
||||
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);
|
||||
sf_fixture(opt, root);
|
||||
fconcat(page, sizeof(page), root, "page.html");
|
||||
|
||||
/* Cap between the small assets and big.png. */
|
||||
@@ -5515,13 +5556,10 @@ static int st_singlefile(httrackp *opt, int argc, char **argv) {
|
||||
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");
|
||||
/* Only the marked reference is touched: the value keeps its own quoting, so
|
||||
nothing can be emitted that the attribute could not already hold. */
|
||||
sf_check(strstr(out, "style='content:\"x\"; background:url(data:") != NULL,
|
||||
"style attribute re-quoted instead of substituted in place");
|
||||
|
||||
sf_check(strstr(out, "img/big.png 2x") != NULL, "over-cap asset inlined");
|
||||
sf_check(strstr(out, " 1x") != NULL, "srcset descriptor lost");
|
||||
@@ -5541,7 +5579,7 @@ static int st_singlefile(httrackp *opt, int argc, char **argv) {
|
||||
"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,
|
||||
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");
|
||||
@@ -5605,7 +5643,7 @@ static int st_singlefile(httrackp *opt, int argc, char **argv) {
|
||||
|
||||
/* Same page, a cap above big.png: it now inlines. */
|
||||
fconcat(root, sizeof(root), argv[0], "mirror2/");
|
||||
sf_fixture(root);
|
||||
sf_fixture(opt, root);
|
||||
fconcat(page, sizeof(page), root, "page.html");
|
||||
opt->single_file_max_size = 1024 * 1024;
|
||||
sf_check(singlefile_rewrite_file(opt, root, page),
|
||||
@@ -5619,21 +5657,37 @@ static int st_singlefile(httrackp *opt, int argc, char **argv) {
|
||||
/* 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);
|
||||
sf_fixture(opt, 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");
|
||||
/* The page is still rewritten: nothing inlines, but the marks must go. */
|
||||
sf_check(singlefile_rewrite_file(opt, root, page),
|
||||
"one-byte cap left the marks in place");
|
||||
{
|
||||
char *capped = readfile_utf8(page);
|
||||
|
||||
/* Only the two data: URIs the fixture itself ships. */
|
||||
sf_check(capped != NULL && sf_count(capped, ";base64,") == 2,
|
||||
"one-byte cap inlined");
|
||||
freet(capped);
|
||||
}
|
||||
{
|
||||
String verbatim = STRING_EMPTY;
|
||||
|
||||
StringClear(verbatim);
|
||||
(void) singlefile_rewrite_html(opt, root, page, sf_page,
|
||||
sizeof(sf_page) - 1,
|
||||
String marked = STRING_EMPTY;
|
||||
|
||||
sf_expand_fixture(opt, sf_page, sizeof(sf_page) - 1, &marked);
|
||||
(void) singlefile_rewrite_html(opt, root, page, StringBuff(marked),
|
||||
StringLength(marked),
|
||||
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");
|
||||
/* Mark-transparent, not byte-transparent: a reference that cannot be
|
||||
inlined loses its mark and keeps everything else. */
|
||||
sf_check(StringLength(verbatim) < StringLength(marked),
|
||||
"a page with nothing to inline kept its marks");
|
||||
sf_check(strstr(StringBuff(verbatim), singlefile_intro(opt)) == NULL,
|
||||
"an un-inlinable reference kept its mark");
|
||||
StringFree(marked);
|
||||
StringFree(verbatim);
|
||||
}
|
||||
(void) outlen;
|
||||
@@ -5642,26 +5696,29 @@ static int st_singlefile(httrackp *opt, int argc, char **argv) {
|
||||
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;
|
||||
static const char bomb_css[] =
|
||||
"@import \"\001b.css\002\";@import \"\001b.css\002\";"
|
||||
"@import \"\001b.css\002\";@import \"\001b.css\002\";\n";
|
||||
static const char bomb_html[] =
|
||||
"<html><head>"
|
||||
"<link rel=\"stylesheet\" href=\"\001b.css\002\">"
|
||||
"</head></html>\n";
|
||||
size_t css_len;
|
||||
String small = STRING_EMPTY, large = STRING_EMPTY, bomb = STRING_EMPTY;
|
||||
|
||||
fconcat(root, sizeof(root), argv[0], "bomb/");
|
||||
sf_put(root, "b.css", bomb_css, css_len);
|
||||
css_len = sf_put_marked(opt, root, "b.css", bomb_css, sizeof(bomb_css) - 1);
|
||||
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,
|
||||
sf_expand_fixture(opt, bomb_html, sizeof(bomb_html) - 1, &bomb);
|
||||
(void) singlefile_rewrite_html(opt, root, page, StringBuff(bomb),
|
||||
StringLength(bomb), (LLint) css_len * 3,
|
||||
&small);
|
||||
(void) singlefile_rewrite_html(opt, root, page, bomb_html,
|
||||
sizeof(bomb_html) - 1,
|
||||
SINGLEFILE_MAX_PAGE_SIZE, &large);
|
||||
(void) singlefile_rewrite_html(opt, root, page, StringBuff(bomb),
|
||||
StringLength(bomb), 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");
|
||||
@@ -5671,6 +5728,7 @@ static int st_singlefile(httrackp *opt, int argc, char **argv) {
|
||||
"the per-page budget was not charged as each asset was taken");
|
||||
StringFree(small);
|
||||
StringFree(large);
|
||||
StringFree(bomb);
|
||||
}
|
||||
|
||||
if (!sf_colon_ok)
|
||||
|
||||
1017
src/htssinglefile.c
1017
src/htssinglefile.c
File diff suppressed because it is too large
Load Diff
@@ -55,18 +55,62 @@ extern "C" {
|
||||
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. */
|
||||
/* The mark htsparse appends to a saved reference the pass may inline:
|
||||
|
||||
#!<16-hex secret>.<class>.<len>
|
||||
|
||||
A fragment, so a mirror left marked by an interrupted run still browses.
|
||||
The secret is 64 CSPRNG bits drawn once per run and never written to disk,
|
||||
which is what makes the mark unforgeable: a site cannot spell one, so no
|
||||
sanitiser has to keep hostile bytes away from it. <len> is the byte length
|
||||
of the reference text immediately preceding the mark, so the pass never has
|
||||
to guess where that reference starts. <class> is the context htsparse saw,
|
||||
checked against the resolved type so a mismatch fails loudly. */
|
||||
#define SINGLEFILE_MARK_INTRO "#!"
|
||||
#define SINGLEFILE_SECRET_HEX 16
|
||||
|
||||
/* <class>: what the referencing context expects. */
|
||||
#define SINGLEFILE_CLASS_ANY '-'
|
||||
#define SINGLEFILE_CLASS_CSS 'c'
|
||||
#define SINGLEFILE_CLASS_JS 'j'
|
||||
|
||||
/* Enough for the intro, the secret, both separators and a 20-digit length. */
|
||||
#define SINGLEFILE_MARK_MAX 64
|
||||
|
||||
/* Release this run's secret. */
|
||||
void singlefile_free(httrackp *opt);
|
||||
|
||||
/* "#!<secret>" for this run, or NULL if no CSPRNG was available (in which case
|
||||
nothing may be marked). Draws the secret on first use. */
|
||||
const char *singlefile_intro(httrackp *opt);
|
||||
|
||||
/* HTS_FALSE if [body,len) already contains this run's intro, in which case the
|
||||
document must not be marked: RFC 2046 gives the generator the same duty for
|
||||
a MIME boundary. Only a 2^-64 coincidence can trip it, since the intro is
|
||||
not something fetched content can spell. */
|
||||
hts_boolean singlefile_may_mark(httrackp *opt, const char *body, size_t len);
|
||||
|
||||
/* Write the mark for a reference of reflen bytes into buf; returns buf. */
|
||||
const char *singlefile_mark(httrackp *opt, char *buf, size_t bufsize, char cls,
|
||||
size_t reflen);
|
||||
|
||||
/* The class a reference in this context may inline as, or 0 to leave it alone.
|
||||
tag_name points just past the '<' of the enclosing start tag, or NULL when
|
||||
there is none (inside a stylesheet or a script); attr at the attribute name.
|
||||
Everything htsparse detects is inlinable unless it names a page. */
|
||||
char singlefile_ref_class(const char *tag_name, const char *attr);
|
||||
|
||||
/* Rewrite every HTML page the mirror produced, then strip the marks left in
|
||||
the assets. 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
|
||||
/* Expand the marks in the 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 a 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. */
|
||||
Returns HTS_TRUE if at least one reference was replaced. */
|
||||
hts_boolean singlefile_rewrite_html(httrackp *opt, const char *root,
|
||||
const char *page_path, const char *html,
|
||||
size_t html_len, LLint page_budget,
|
||||
|
||||
@@ -41,8 +41,14 @@ Please visit our Website: http://www.httrack.com
|
||||
#include "htsstrings.h"
|
||||
#include "htscharset.h"
|
||||
#ifdef _WIN32
|
||||
/* before <stdlib.h>, which is what declares rand_s under it */
|
||||
#define _CRT_RAND_S
|
||||
#include "windows.h"
|
||||
#else
|
||||
#include <errno.h>
|
||||
#ifdef HAVE_SYS_RANDOM_H
|
||||
#include <sys/random.h>
|
||||
#endif
|
||||
#include <dirent.h>
|
||||
#ifdef HAVE_UNISTD_H
|
||||
#include <unistd.h>
|
||||
@@ -1443,3 +1449,59 @@ HTSEXT_API hts_boolean hts_findissystem(find_handle find) {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
hts_boolean hts_random_bytes(void *buf, size_t len) {
|
||||
unsigned char *const p = (unsigned char *) buf;
|
||||
|
||||
#ifdef _WIN32
|
||||
size_t i;
|
||||
|
||||
/* rand_s() is the CRT's wrapper over the system CSPRNG, and unlike
|
||||
BCryptGenRandom it needs no extra import library. */
|
||||
for (i = 0; i < len; i += sizeof(unsigned int)) {
|
||||
const size_t left = len - i;
|
||||
unsigned int v;
|
||||
|
||||
if (rand_s(&v) != 0)
|
||||
return HTS_FALSE;
|
||||
memcpy(p + i, &v, left < sizeof(v) ? left : sizeof(v));
|
||||
}
|
||||
return HTS_TRUE;
|
||||
#else
|
||||
size_t got = 0;
|
||||
FILE *fp;
|
||||
|
||||
#ifdef HAVE_GETRANDOM
|
||||
while (got < len) {
|
||||
const ssize_t n = getrandom(p + got, len - got, 0);
|
||||
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
break; /* pre-3.17 kernel or a seccomp filter: try /dev/urandom */
|
||||
}
|
||||
got += (size_t) n;
|
||||
}
|
||||
if (got == len)
|
||||
return HTS_TRUE;
|
||||
#endif
|
||||
fp = fopen("/dev/urandom", "rb");
|
||||
if (fp == NULL)
|
||||
return HTS_FALSE;
|
||||
got += fread(p + got, 1, len - got, fp);
|
||||
fclose(fp);
|
||||
return got == len ? HTS_TRUE : HTS_FALSE;
|
||||
#endif
|
||||
}
|
||||
|
||||
hts_boolean hts_rename_over(const char *src, const char *dst) {
|
||||
char csrc[CATBUFF_SIZE], cdst[CATBUFF_SIZE];
|
||||
|
||||
fconv(csrc, sizeof(csrc), src);
|
||||
fconv(cdst, sizeof(cdst), dst);
|
||||
if (RENAME(csrc, cdst) == 0)
|
||||
return HTS_TRUE;
|
||||
/* RENAME does not clobber an existing target on Windows. */
|
||||
(void) UNLINK(cdst);
|
||||
return RENAME(csrc, cdst) == 0 ? HTS_TRUE : HTS_FALSE;
|
||||
}
|
||||
|
||||
@@ -137,6 +137,14 @@ HTSEXT_API hts_boolean hts_findisdir(find_handle find);
|
||||
HTSEXT_API hts_boolean hts_findisfile(find_handle find);
|
||||
HTSEXT_API hts_boolean hts_findissystem(find_handle find);
|
||||
|
||||
/* Move src onto dst, replacing an existing dst; HTS_TRUE on success. Both
|
||||
paths are fconv()'d. */
|
||||
hts_boolean hts_rename_over(const char *src, const char *dst);
|
||||
|
||||
/* Fill buf with len cryptographically strong random bytes. HTS_FALSE when no
|
||||
such source is available: the caller must fail, never fall back to rand(). */
|
||||
hts_boolean hts_random_bytes(void *buf, size_t len);
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -964,16 +964,6 @@ static zipFile wacz_zip_open(const char *path) {
|
||||
return zipOpen2_64(path, 0 /*create*/, NULL, &ff);
|
||||
}
|
||||
|
||||
/* Move src onto dst (UTF-8/Windows-safe); RENAME won't clobber on Windows, so
|
||||
fall back to unlink+rename. Returns 0 on success. */
|
||||
static int wacz_rename_over(const char *src, const char *dst) {
|
||||
char cs[CATBUFF_SIZE], cd[CATBUFF_SIZE];
|
||||
if (RENAME(fconv(cs, sizeof(cs), src), fconv(cd, sizeof(cd), dst)) == 0)
|
||||
return 0;
|
||||
(void) UNLINK(fconv(cd, sizeof(cd), dst));
|
||||
return RENAME(fconv(cs, sizeof(cs), src), fconv(cd, sizeof(cd), dst));
|
||||
}
|
||||
|
||||
/* Package the segment(s) + .cdx + a generated pages.jsonl into <base>.wacz at
|
||||
crawl end (the archive file(s) and .cdx are already closed on disk). */
|
||||
static void warc_wacz_package(warc_writer *w) {
|
||||
@@ -1092,7 +1082,7 @@ static void warc_wacz_package(warc_writer *w) {
|
||||
hts_log_print(w->opt, LOG_WARNING,
|
||||
"WACZ: packaging failed, kept existing %s untouched",
|
||||
waczpath);
|
||||
} else if (wacz_rename_over(tmppath, waczpath) != 0) {
|
||||
} else if (!hts_rename_over(tmppath, waczpath)) {
|
||||
(void) UNLINK(fconv(catbuff, sizeof(catbuff), tmppath));
|
||||
hts_log_print(w->opt, LOG_WARNING | LOG_ERRNO,
|
||||
"WACZ: could not finalize %s", waczpath);
|
||||
|
||||
@@ -61,11 +61,6 @@ Please visit our Website: http://www.httrack.com
|
||||
assertf((*opt->filters.filptr) < opt->maxfilter); \
|
||||
} while (0)
|
||||
|
||||
typedef struct htspair_t {
|
||||
const char *tag;
|
||||
const char *attr;
|
||||
} htspair_t;
|
||||
|
||||
/* "embedded" */
|
||||
htspair_t hts_detect_embed[] = {
|
||||
{"img", "src"},
|
||||
@@ -133,11 +128,13 @@ int hts_acceptlink(httrackp * opt, int ptr,
|
||||
return forbidden_url;
|
||||
}
|
||||
|
||||
static int cmp_token(const char *tag, const char *cmp) {
|
||||
hts_boolean hts_cmp_tag_token(const char *tag, const char *cmp) {
|
||||
int p;
|
||||
|
||||
return (strncasecmp(tag, cmp, (p = (int) strlen(cmp))) == 0
|
||||
&& !isalnum((unsigned char) tag[p]));
|
||||
return (tag != NULL && strncasecmp(tag, cmp, (p = (int) strlen(cmp))) == 0 &&
|
||||
!isalnum((unsigned char) tag[p]))
|
||||
? HTS_TRUE
|
||||
: HTS_FALSE;
|
||||
}
|
||||
|
||||
/* TRUE if (tag, attribute) matches an embedded-asset pair in the table */
|
||||
@@ -145,7 +142,8 @@ static hts_boolean is_embed_pair(const htspair_t *table, const char *tag,
|
||||
const char *attribute) {
|
||||
int i;
|
||||
for (i = 0; table[i].tag != NULL; i++) {
|
||||
if (cmp_token(tag, table[i].tag) && cmp_token(attribute, table[i].attr))
|
||||
if (hts_cmp_tag_token(tag, table[i].tag) &&
|
||||
hts_cmp_tag_token(attribute, table[i].attr))
|
||||
return HTS_TRUE;
|
||||
}
|
||||
return HTS_FALSE;
|
||||
|
||||
@@ -56,6 +56,19 @@ hts_boolean hts_robots_forbids(httrackp *opt, const char *adr, const char *fil,
|
||||
hts_boolean filters_decided,
|
||||
hts_boolean filters_refused);
|
||||
|
||||
/* A (tag, attribute) pair naming a reference kind. */
|
||||
#ifndef HTS_DEF_DEFSTRUCT_htspair_t
|
||||
#define HTS_DEF_DEFSTRUCT_htspair_t
|
||||
|
||||
typedef struct htspair_t {
|
||||
const char *tag;
|
||||
const char *attr;
|
||||
} htspair_t;
|
||||
#endif
|
||||
|
||||
/* HTS_TRUE if tag starts with the whole token cmp; NULL tag never matches. */
|
||||
hts_boolean hts_cmp_tag_token(const char *tag, const char *cmp);
|
||||
|
||||
int hts_acceptlink(httrackp * opt, int ptr,
|
||||
const char *adr, const char *fil,
|
||||
const char *tag, const char *attribute,
|
||||
|
||||
@@ -18,6 +18,7 @@ set -euo pipefail
|
||||
bash "$top_srcdir/tests/local-crawl.sh" \
|
||||
--rerun-args '--update -M400000' \
|
||||
--log-found 'More than 400000 bytes have been transferred.. giving up' \
|
||||
--log-not-found 'could not restore' \
|
||||
--found bigtrunc/slow.bin \
|
||||
--file-min-bytes bigtrunc/slow.bin 655360 \
|
||||
--file-min-bytes bigtrunc/fast.bin 655360 \
|
||||
|
||||
@@ -5,11 +5,16 @@
|
||||
# real gate: STORE-mode entries, the fixed layout, recomputed sha256 digests and
|
||||
# the datapackage-digest chain (plus py-wacz/pywb when importable). The crawl
|
||||
# skips cleanly on a build without OpenSSL (no conformant SHA-256 -> no package).
|
||||
#
|
||||
# The cacheless second pass repackages over the .wacz the first one left: the
|
||||
# clobber, not the happy rename, is what breaks when a platform refuses to
|
||||
# rename onto an existing file (#726).
|
||||
|
||||
set -eu
|
||||
|
||||
: "${top_srcdir:=..}"
|
||||
|
||||
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 --wacz-validate \
|
||||
--rerun-args '-C0' --log-not-found 'could not finalize' \
|
||||
--found 'mini304/index.html' --found 'mini304/page.html' \
|
||||
httrack 'BASEURL/mini304/index.html' --warc-file warc-out --wacz
|
||||
|
||||
@@ -37,6 +37,11 @@ cat >"${doc}/index.html" <<'EOF'
|
||||
<img src="pixel.svg" data-src="lazy.svg" alt="l">
|
||||
<script src="app.js"></script>
|
||||
<a href="other.html">other</a>
|
||||
<p>id=pixel.svg#!htsinline end</p>
|
||||
<link rel="stylesheet" href="style_noext">
|
||||
<script src="app_noext"></script>
|
||||
<img src="pixel.svg#icon">
|
||||
<a href="pixel.svg">raw</a>
|
||||
</body></html>
|
||||
EOF
|
||||
cat >"${doc}/other.html" <<'EOF'
|
||||
@@ -50,6 +55,8 @@ cat >"${doc}/nested.css" <<'EOF'
|
||||
div { background: url(pixel.svg); }
|
||||
EOF
|
||||
printf 'var app = 1;\n' >"${doc}/app.js"
|
||||
printf 'body { color: red; }\n' >"${doc}/style_noext"
|
||||
printf 'var noext = 1;\n' >"${doc}/app_noext"
|
||||
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' \
|
||||
@@ -257,12 +264,107 @@ 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"
|
||||
echo OK
|
||||
|
||||
printf '[a page cannot forge a mark] ..\t'
|
||||
# The page ships what the old in-band marker looked like, against a real
|
||||
# mirrored asset. The mark now carries a per-run secret the site cannot spell,
|
||||
# so the text is not a mark and is not even rewritten: byte-identical to the
|
||||
# mirror taken without --single-file. pixel.svg is inlined elsewhere on the
|
||||
# page, so the encoder was demonstrably willing.
|
||||
forged=$(sed -n 's/.*<p>\(id=[^<]*\)<\/p>.*/\1/p' "$page")
|
||||
test "$forged" = 'id=pixel.svg#!htsinline end' || {
|
||||
echo "FAIL: the page was altered: $forged"
|
||||
exit 1
|
||||
}
|
||||
grep -q 'data:image/svg+xml;base64,' "$page" || {
|
||||
echo "FAIL: nothing was inlined at all, so the probe proves nothing"
|
||||
exit 1
|
||||
}
|
||||
echo OK
|
||||
|
||||
printf '[a mark cannot ride in on a header] ..\t'
|
||||
# htsparse echoes the Content-Type charset into a <meta>; a forged mark there
|
||||
# bypasses anything that scans the body. Unforgeable means the channel does
|
||||
# not matter, so this asserts on the channel the body sweep could never see.
|
||||
grep -q 'charset=127' "$page" || true
|
||||
if grep -q 'content="text/html;charset=data:' "$page"; then
|
||||
echo "FAIL: a charset-borne mark was expanded"
|
||||
exit 1
|
||||
fi
|
||||
echo OK
|
||||
|
||||
printf '[assets keep no marks] ..\t'
|
||||
if grep -rlF '#!' "${out}/127.0.0.1_${port}" 2>/dev/null | grep -q .; then
|
||||
marked=$(grep -rlF '#!' "${out}/127.0.0.1_${port}" | tr '\n' ' ')
|
||||
# the fixture's own literal is expected in index.html only
|
||||
test "$marked" = "${out}/127.0.0.1_${port}/index.html " || {
|
||||
echo "FAIL: marks survived in $marked"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
echo OK
|
||||
|
||||
printf '[an unrecognised type still inlines from its context] ..\t'
|
||||
# style_noext and app_noext have no usable extension, so only the referencing
|
||||
# context says what they are. plain.css/plain.js are the control: they inline
|
||||
# either way, so a failure here is the context path and not the encoder.
|
||||
grep -q 'href="data:text/css;base64,' "$page" || {
|
||||
echo "FAIL: no stylesheet inlined at all"
|
||||
exit 1
|
||||
}
|
||||
if grep -q 'href="style_noext"' "$page"; then
|
||||
echo "FAIL: extensionless stylesheet was not inlined"
|
||||
exit 1
|
||||
fi
|
||||
if grep -q 'src="app_noext"' "$page"; then
|
||||
echo "FAIL: extensionless script was not inlined"
|
||||
exit 1
|
||||
fi
|
||||
echo OK
|
||||
|
||||
printf '[a fragment survives onto the data: URI] ..\t'
|
||||
# An SVG fragment selects a view, so dropping it renders the wrong thing (#766).
|
||||
grep -q 'data:image/svg+xml;base64,[A-Za-z0-9+/=]*#icon' "$page" || {
|
||||
echo "FAIL: the fragment was dropped"
|
||||
exit 1
|
||||
}
|
||||
echo OK
|
||||
|
||||
printf '[a page link is not inlined even when it names an asset] ..\t'
|
||||
# <a href> to an image: only the deny rule stops this, since the MIME gate
|
||||
# would happily take it. Without the rule the anchor becomes a data: URI.
|
||||
grep -q '<a href="pixel.svg">raw</a>' "$page" || {
|
||||
echo "FAIL: an anchor to an inlinable asset was rewritten"
|
||||
exit 1
|
||||
}
|
||||
echo OK
|
||||
|
||||
printf '[-%%M refuses to pair with --single-file] ..\t'
|
||||
both=$(httrack -O "${tmpdir}/both" --quiet --robots=0 --retries=0 -%M \
|
||||
--single-file "${base}/index.html" 2>&1 || true)
|
||||
case "$both" in
|
||||
*"pick one"*) echo OK ;;
|
||||
*)
|
||||
echo "FAIL: the combination was accepted: $both"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
printf '[--changes and --single-file together] ..\t'
|
||||
# Both hook end-of-mirror. Expansion must run first, so the report sees the
|
||||
# inlined size rather than the marked intermediate.
|
||||
chg="${tmpdir}/chg"
|
||||
mkdir "$chg"
|
||||
httrack "${common[@]}" --single-file --changes "${base}/index.html" \
|
||||
>"${tmpdir}/log6" 2>&1
|
||||
chgpage="${chg}/127.0.0.1_${port}/index.html"
|
||||
test ! -f "$chgpage" || grep -q 'base64,' "$chgpage" || {
|
||||
echo "FAIL: --changes suppressed the inlining"
|
||||
exit 1
|
||||
}
|
||||
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
|
||||
|
||||
@@ -57,6 +57,8 @@ rerun_dead=
|
||||
tmpdir=
|
||||
serverpid=
|
||||
crawlpid=
|
||||
wacz_poisoned=
|
||||
wacz_poison="stale-wacz-that-a-second-pass-must-replace"
|
||||
|
||||
function warning {
|
||||
echo "** $*" >&2
|
||||
@@ -272,6 +274,14 @@ if test -n "$warc_validate"; then
|
||||
test -z "$w1" || cp "$w1" "${tmpdir}/warc-pass1.gz"
|
||||
fi
|
||||
|
||||
# Poison the first-pass .wacz when a second pass follows: repackaging moves the
|
||||
# new archive over it, so the marker must be gone afterwards (#726). Poisoning
|
||||
# beats comparing the two packages, which can come out byte-identical.
|
||||
if test -n "$wacz_validate" && test -n "${rerun}${rerun_args}"; then
|
||||
wacz_poisoned=$(find "$mirrorroot" -maxdepth 2 -name '*.wacz' 2>/dev/null | sort | tail -n1)
|
||||
test -z "$wacz_poisoned" || echo "$wacz_poison" >"$wacz_poisoned"
|
||||
fi
|
||||
|
||||
# --- optional second pass: re-mirror into the same dir (cache/update path) ----
|
||||
if test -n "$rerun"; then
|
||||
info "re-running httrack (update pass)"
|
||||
@@ -416,6 +426,12 @@ if test -n "$wacz_validate"; then
|
||||
fi
|
||||
die "no .wacz file produced under $mirrorroot"
|
||||
fi
|
||||
if test -n "$wacz_poisoned"; then
|
||||
info "checking the second pass replaced the .wacz"
|
||||
grep -q "$wacz_poison" "$wacz_poisoned" 2>/dev/null &&
|
||||
die "stale .wacz kept: $wacz_poisoned"
|
||||
result "OK"
|
||||
fi
|
||||
validator=$(nativepath "${testdir}/wacz-validate.py")
|
||||
info "validating WACZ package"
|
||||
"$python" "$validator" "$(nativepath "$wacz")" >&2 || die "WACZ validation failed"
|
||||
|
||||
Reference in New Issue
Block a user