BentoPDF for bau-pm, without third-party hosts (bau-pm BAU-119)
BentoPDF v2.8.8 built from source at its pinned commit with BentoPDF's own air-gapped build options, every module it would load from a CDN vendored and checked against vendor.lock, a Content-Security-Policy without a single host, digital-sign-pdf and timestamp-pdf disabled because both need a third party, and the hand-off glue on every page: the AGPL source link, the hand-in of a file from bau-pm through a one-minute cookie into sessionStorage, confirmed by the tool's reaction, the hand-back of a result from every window, and the signing tool's flatten switch locked on. The zero-host gate drives all 117 enabled tool pages in Chromium and fails on any request to another origin, any CSP violation, a CSP that names a host, and a disabled id that names no page. Green on this image (3 min 52 s on a private network), red on the stock upstream image (115 findings) and on a misspelled id. CI runs it on every push, the release workflow before it pushes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
47138ddfbe
19 files changed
+1733
No files matched your search
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/bin/sh
|
||||
# What this repository adds to BentoPDF's finished build, run in the
|
||||
# Dockerfile's builder stage after `npm run build`:
|
||||
#
|
||||
# finish.sh <app-dir>
|
||||
#
|
||||
# 1. a <script src="/bau-pm/handoff.js"> in every HTML page, the PDF.js
|
||||
# viewers' pages included (the sign tool saves from inside one of them);
|
||||
# 2. the precompressed copies of those pages regenerated, because nginx's
|
||||
# gzip_static would otherwise serve the old page without the tag;
|
||||
# 3. every host taken out of the generated Content-Security-Policy, so the
|
||||
# pages may load from their own origin and from nowhere else.
|
||||
#
|
||||
# Each step checks its own result and fails the build when it did not happen.
|
||||
set -eu
|
||||
|
||||
app=$1
|
||||
dist=$app/dist
|
||||
tag='<script src="/bau-pm/handoff.js"></script>'
|
||||
|
||||
pages=$(find "$dist" -name '*.html' | wc -l)
|
||||
[ "$pages" -gt 100 ] || { echo "finish: only $pages pages in $dist" >&2; exit 1; }
|
||||
|
||||
find "$dist" -name '*.html' | while read -r page; do
|
||||
grep -q '</head>' "$page" || { echo "finish: $page has no </head>" >&2; exit 1; }
|
||||
sed -i "s#</head>#$tag</head>#" "$page"
|
||||
rm -f "$page.gz" "$page.br"
|
||||
gzip -9 -k -n "$page"
|
||||
done
|
||||
# find -exec rather than grep --include, which busybox's grep does not know.
|
||||
tagged=$(find "$dist" -name '*.html' -exec grep -lF "$tag" {} + | wc -l)
|
||||
[ "$tagged" -eq "$pages" ] || { echo "finish: $tagged of $pages pages carry the tag" >&2; exit 1; }
|
||||
stale=$(find "$dist" -name '*.html.br' | wc -l)
|
||||
[ "$stale" -eq 0 ] || { echo "finish: $stale brotli copies left" >&2; exit 1; }
|
||||
|
||||
for conf in security-headers.conf security-headers-docs.conf; do
|
||||
sed -E -i 's# https?:(//[^ ;"]*)?##g' "$app/$conf"
|
||||
if grep -Eq 'https?:' "$app/$conf"; then
|
||||
echo "finish: $conf still names a host:" >&2
|
||||
grep -Eo 'https?:[^ ;"]*' "$app/$conf" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "finish: $tagged pages tagged, CSP without hosts"
|
||||
Executable
+101
@@ -0,0 +1,101 @@
|
||||
#!/bin/sh
|
||||
# Lays out every module BentoPDF would otherwise fetch from a CDN under one
|
||||
# directory, in the layout BentoPDF's own air-gap setup produces
|
||||
# (scripts/prepare-airgap.sh upstream, its generated setup.sh). Runs in the
|
||||
# Dockerfile's vendor stage.
|
||||
#
|
||||
# vendor.sh <bentopdf-source> <vendor.lock> <out-dir>
|
||||
#
|
||||
# Every package comes through `npm pack --json`, which verifies the tarball
|
||||
# against the registry; this script then compares that integrity with the one
|
||||
# committed in vendor.lock and fails the build on any difference, so a package
|
||||
# cannot change under a pinned version unnoticed. A missing input fails too:
|
||||
# nothing here skips.
|
||||
set -eu
|
||||
|
||||
src=$1
|
||||
lock=$2
|
||||
out=$3
|
||||
|
||||
OCR_FONT_URL=https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSans/NotoSans-Regular.ttf
|
||||
OCR_FONT_SHA256=b85c38ecea8a7cfb39c24e395a4007474fa5a4fc864f6ee33309eb4948d232d5
|
||||
|
||||
work=$(mktemp -d)
|
||||
mkdir -p "$out/pymupdf" "$out/gs" "$out/cpdf" "$out/ocr/core" "$out/ocr/lang-data" \
|
||||
"$out/ocr/fonts" "$out/embedpdf"
|
||||
|
||||
# Packs one spec into $work and checks it against the lock. Prints the tarball.
|
||||
# Always called as an assignment (t=$(pack ...)): an exit inside a command
|
||||
# substitution only ends the subshell, and only an assignment hands its status
|
||||
# to `set -e`.
|
||||
pack() {
|
||||
spec=$1
|
||||
want=$(awk -v s="$spec" '$1 == s { print $2 }' "$lock")
|
||||
[ -n "$want" ] || { echo "vendor: $spec is not in $lock" >&2; exit 1; }
|
||||
json=$(cd "$work" && npm pack --json --silent "$spec")
|
||||
got=$(printf '%s' "$json" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const p=JSON.parse(s)[0];process.stdout.write(p.integrity+"\t"+p.filename)})')
|
||||
[ "${got%% *}" = "$want" ] || {
|
||||
echo "vendor: $spec integrity ${got%% *} differs from the lock's $want" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "$work/${got#* }"
|
||||
}
|
||||
|
||||
# Unpacks a tarball's package/<sub> into a directory.
|
||||
unpack() {
|
||||
tarball=$1 sub=$2 dest=$3
|
||||
tmp=$(mktemp -d)
|
||||
tar -xzf "$tarball" -C "$tmp"
|
||||
[ -e "$tmp/package/$sub" ] || { echo "vendor: $tarball has no package/$sub" >&2; exit 1; }
|
||||
cp -R "$tmp/package/$sub"/. "$dest"/
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
|
||||
# PyMuPDF and Ghostscript ship inside BentoPDF's repository, at the pinned commit.
|
||||
set -- "$src"/bentopdf-airgap-bundle/bentopdf-pymupdf-wasm-*.tgz
|
||||
[ -f "$1" ] || { echo "vendor: no PyMuPDF tarball in $src/bentopdf-airgap-bundle" >&2; exit 1; }
|
||||
unpack "$1" . "$out/pymupdf"
|
||||
set -- "$src"/bentopdf-airgap-bundle/bentopdf-gs-wasm-*.tgz
|
||||
[ -f "$1" ] || { echo "vendor: no Ghostscript tarball in $src/bentopdf-airgap-bundle" >&2; exit 1; }
|
||||
# Two loaders read this one directory: BentoPDF's own wants gs.js and gs.wasm
|
||||
# at its root (upstream's air-gap layout copies only assets/), PyMuPDF's wrapper
|
||||
# wants the package's dist/index.js under it - pdf-to-docx asked for
|
||||
# /wasm/gs/dist/index.js and got a 404. The whole package, plus links at the
|
||||
# root, serves both from one copy.
|
||||
unpack "$1" . "$out/gs"
|
||||
ln -s assets/gs.js "$out/gs/gs.js"
|
||||
ln -s assets/gs.wasm "$out/gs/gs.wasm"
|
||||
|
||||
t=$(pack coherentpdf@2.5.5)
|
||||
unpack "$t" dist "$out/cpdf"
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
t=$(pack tesseract.js@7.0.0)
|
||||
tar -xzf "$t" -C "$tmp"
|
||||
cp "$tmp/package/dist/worker.min.js" "$out/ocr/worker.min.js"
|
||||
rm -rf "$tmp"
|
||||
t=$(pack tesseract.js-core@7.0.0)
|
||||
unpack "$t" . "$out/ocr/core"
|
||||
for lang in deu fra ita eng; do
|
||||
tmp=$(mktemp -d)
|
||||
t=$(pack "@tesseract.js-data/$lang@1.0.0")
|
||||
tar -xzf "$t" -C "$tmp"
|
||||
cp "$tmp/package/4.0.0_best_int/$lang.traineddata.gz" "$out/ocr/lang-data/"
|
||||
rm -rf "$tmp"
|
||||
done
|
||||
|
||||
wget -q -O "$out/ocr/fonts/NotoSans-Regular.ttf" "$OCR_FONT_URL"
|
||||
echo "$OCR_FONT_SHA256 $out/ocr/fonts/NotoSans-Regular.ttf" | sha256sum -c -s || {
|
||||
echo "vendor: the OCR font's checksum differs from $OCR_FONT_SHA256" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# The PDF editor asks for <base>/fonts-latin@1.0.0/fonts/NotoSans-Regular.ttf.
|
||||
for pkg in latin arabic hebrew; do
|
||||
mkdir -p "$out/embedpdf/fonts-$pkg@1.0.0"
|
||||
t=$(pack "@embedpdf/fonts-$pkg@1.0.0")
|
||||
unpack "$t" . "$out/embedpdf/fonts-$pkg@1.0.0"
|
||||
done
|
||||
|
||||
rm -rf "$work"
|
||||
echo "vendor: $(find "$out" -type f | wc -l) files, $(du -sh "$out" | cut -f1)"
|
||||
Reference in new issue
Block a user