mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-10-19 14:17:20 +02:00
fix: prevent large notebook download timeouts
This commit is contained in:
3 files changed
+183
-40
No files matched your search
@@ -442,12 +442,31 @@ function replaceTerm(content: string, term: CompiledTermVariant): string {
|
||||
if (term.pattern instanceof RegExp) {
|
||||
return content.replace(term.pattern, () => term.mask);
|
||||
}
|
||||
const matcher = term.pattern.matcher(content);
|
||||
// Fixed-width literal searches run natively. Validate each candidate with
|
||||
// RE2 on a small window to preserve its case folding and boundary semantics
|
||||
// without scanning megabytes of notebook outputs in the JS regex engine.
|
||||
const candidates = term.literalPrefilter
|
||||
? content.matchAll(new RegExp(term.literalPrefilter.source, "giu"))
|
||||
: null;
|
||||
const matcher = candidates ? null : term.pattern.matcher(content);
|
||||
const pieces: string[] = [];
|
||||
let cursor = 0;
|
||||
while (matcher.find()) {
|
||||
const start = matcher.start();
|
||||
const end = matcher.end();
|
||||
const matches = function* () {
|
||||
if (candidates) {
|
||||
for (const candidate of candidates) {
|
||||
const start = candidate.index!;
|
||||
const end = start + candidate[0].length;
|
||||
const offset = Math.max(0, start - 2);
|
||||
const check = (term.pattern as RE2JS).matcher(content.slice(offset, end + 2));
|
||||
if (check.find(start - offset) && check.start() === start - offset && check.end() === end - offset) {
|
||||
yield { start, end };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while (matcher!.find()) yield { start: matcher!.start(), end: matcher!.end() };
|
||||
}
|
||||
};
|
||||
for (const { start, end } of matches()) {
|
||||
// RE2 has no lookahead. Check the generated Unicode word boundaries
|
||||
// outside the engine, without executing any user-supplied native regex.
|
||||
if (term.before && /[\p{L}\p{N}_]$/u.test(content.slice(Math.max(0, start - 2), start))) continue;
|
||||
|
||||
+31
-36
@@ -1,4 +1,5 @@
|
||||
import got from "got";
|
||||
import { Readable, Transform } from "stream";
|
||||
import { Parse } from "unzip-stream";
|
||||
import archiver = require("archiver");
|
||||
|
||||
@@ -112,33 +113,24 @@ export async function streamAnonymizedZip(
|
||||
}
|
||||
const downloadStream = got.stream(response.url);
|
||||
|
||||
res.on("error", (error) => {
|
||||
logger.error("response stream error", serializeError(error));
|
||||
downloadStream.destroy();
|
||||
});
|
||||
res.on("close", () => {
|
||||
downloadStream.destroy();
|
||||
});
|
||||
|
||||
const archive = archiver("zip", {});
|
||||
const parser = Parse() as Transform;
|
||||
const activeStreams = new Set<Readable>();
|
||||
const compiledTerms = compileTerms(opt.anonymizerOptions.terms || []);
|
||||
|
||||
// Track whether the upstream zipball finished cleanly. If it didn't,
|
||||
// we must NOT finalize the archive — finalizing while bytes are still
|
||||
// flowing to the response produces a valid-looking ZIP that's missing
|
||||
// entries, which the client has no way to detect (status 200, archive
|
||||
// opens). Destroy the response instead so the client sees a connection
|
||||
// drop and knows the download failed. Same class of silent-truncation
|
||||
// bug as #694.
|
||||
let failed = false;
|
||||
const parser = Parse();
|
||||
const fail = (error: Error) => {
|
||||
if (failed) return;
|
||||
failed = true;
|
||||
logger.error("upstream zipball failed", serializeError(error));
|
||||
let stopped = false;
|
||||
const stop = () => {
|
||||
if (stopped) return;
|
||||
stopped = true;
|
||||
downloadStream.destroy();
|
||||
downloadStream.unpipe(parser);
|
||||
parser.destroy();
|
||||
for (const stream of activeStreams) stream.destroy();
|
||||
activeStreams.clear();
|
||||
archive.abort();
|
||||
};
|
||||
const fail = (error: Error) => {
|
||||
if (stopped) return;
|
||||
logger.error("zip stream failed", serializeError(error));
|
||||
stop();
|
||||
const destroyable = res as unknown as {
|
||||
destroy?: (err?: Error) => void;
|
||||
end?: () => void;
|
||||
@@ -149,14 +141,23 @@ export async function streamAnonymizedZip(
|
||||
destroyable.end();
|
||||
}
|
||||
};
|
||||
// pipe() returns the destination. Listen on the archive itself as well,
|
||||
// including errors emitted after the upstream ZIP has finished downloading.
|
||||
|
||||
res.on("error", (error) => fail(error as Error));
|
||||
res.on("close", stop);
|
||||
archive.on("error", fail);
|
||||
archive.pipe(res);
|
||||
|
||||
downloadStream
|
||||
.on("error", fail)
|
||||
.pipe(parser)
|
||||
.on("entry", (entry: NodeJS.ReadableStream & { type: string; path: string; autodrain: () => void }) => {
|
||||
.on("entry", (entry: Readable & { type: string; path: string; autodrain: () => void }) => {
|
||||
if (stopped) {
|
||||
entry.destroy();
|
||||
return;
|
||||
}
|
||||
entry.on("error", fail);
|
||||
activeStreams.add(entry);
|
||||
entry.once("close", () => activeStreams.delete(entry));
|
||||
if (entry.type === "File") {
|
||||
try {
|
||||
const fileName = anonymizePathCompiled(
|
||||
@@ -175,12 +176,12 @@ export async function streamAnonymizedZip(
|
||||
...opt.anonymizerOptions,
|
||||
filePath: entry.path,
|
||||
});
|
||||
entry.on("error", fail);
|
||||
anonymizer.on("error", fail);
|
||||
activeStreams.add(anonymizer);
|
||||
anonymizer.once("close", () => activeStreams.delete(anonymizer));
|
||||
const st = entry.pipe(anonymizer);
|
||||
archive.append(st, { name: fileName });
|
||||
} catch (error) {
|
||||
entry.autodrain();
|
||||
fail(error as Error);
|
||||
}
|
||||
} else {
|
||||
@@ -189,13 +190,7 @@ export async function streamAnonymizedZip(
|
||||
})
|
||||
.on("error", fail)
|
||||
.on("finish", () => {
|
||||
if (failed) return;
|
||||
try {
|
||||
archive.finalize().catch(fail);
|
||||
} catch (error) {
|
||||
fail(error as Error);
|
||||
}
|
||||
if (stopped) return;
|
||||
void archive.finalize().catch(fail);
|
||||
});
|
||||
|
||||
archive.pipe(res).on("error", fail);
|
||||
}
|
||||
Reference in new issue
Block a user