OpenCode commited on
Commit ·
65fdff2
1
Parent(s): 35943bd
Harden reader resource handling
Browse files- scripts/check_reader_release.mjs +1 -1
- static/app.js +1 -1
- static/reader-security.js +2 -1
- static/reader.js +3 -2
- static/sw.js +2 -0
scripts/check_reader_release.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
| 2 |
import path from "node:path";
|
| 3 |
import crypto from "node:crypto";
|
| 4 |
const args = Object.fromEntries(process.argv.slice(2).reduce((items, value, index, all) => value.startsWith("--") ? [...items, [value.slice(2), all[index + 1]?.startsWith("--") ? true : all[index + 1]]] : items, []));
|
| 5 |
-
const required = ["reader.html", "reader.css", "reader.js", "reader-contract.js", "reader-store.js", "reader-request-manager.js", "reader-chapter-repository.js", "reader-scroll-anchor.js", "reader-section-virtualizer.js", "reader-runtime.js", "reader-format-adapters.js", "pdf-worker-wrapper.mjs", "foliate-reader/view.js"];
|
| 6 |
const behavioral = ["createReaderRuntime", "createAdapterRegistry", "registerReaderFormatAdapters", "formatAdapters.activate(capability.mode)", "disableStream: true", "nextReaderGeneration(\"navigation\")"];
|
| 7 |
const hash = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"), normalized = (file) => fs.readFileSync(file, "utf8").replaceAll("/search/static/", "/static/").replace(/\s+/g, "");
|
| 8 |
function assert(condition, message) { if (!condition) throw new Error(message); }
|
|
|
|
| 2 |
import path from "node:path";
|
| 3 |
import crypto from "node:crypto";
|
| 4 |
const args = Object.fromEntries(process.argv.slice(2).reduce((items, value, index, all) => value.startsWith("--") ? [...items, [value.slice(2), all[index + 1]?.startsWith("--") ? true : all[index + 1]]] : items, []));
|
| 5 |
+
const required = ["reader.html", "reader.css", "reader.js", "reader-contract.js", "reader-store.js", "reader-request-manager.js", "reader-chapter-repository.js", "reader-scroll-anchor.js", "reader-section-virtualizer.js", "reader-runtime.js", "reader-format-adapters.js", "reader-security.js", "pdf-worker-wrapper.mjs", "foliate-reader/view.js"];
|
| 6 |
const behavioral = ["createReaderRuntime", "createAdapterRegistry", "registerReaderFormatAdapters", "formatAdapters.activate(capability.mode)", "disableStream: true", "nextReaderGeneration(\"navigation\")"];
|
| 7 |
const hash = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"), normalized = (file) => fs.readFileSync(file, "utf8").replaceAll("/search/static/", "/static/").replace(/\s+/g, "");
|
| 8 |
function assert(condition, message) { if (!condition) throw new Error(message); }
|
static/app.js
CHANGED
|
@@ -961,7 +961,7 @@ function warmReaderIntent(rawUrl) {
|
|
| 961 |
extension = (readerUrl.searchParams.get("ext") || "").toLowerCase();
|
| 962 |
sourceUrl = readerUrl.searchParams.get("url") || "";
|
| 963 |
} catch (_) { return; }
|
| 964 |
-
const shellAssets = ["/static/reader.css", "/static/reader-contract.js", "/static/reader-store.js", "/static/reader-request-manager.js", "/static/reader-chapter-repository.js", "/static/reader-scroll-anchor.js", "/static/reader-section-virtualizer.js", "/static/reader-runtime.js", "/static/reader-format-adapters.js", "/static/reader.js"];
|
| 965 |
const engineAssets = extension === "pdf"
|
| 966 |
? ["/static/vendor/pdf.min.f80490490320.mjs", "/static/pdf-worker-wrapper.mjs", "/static/vendor/pdf.worker.min.8ab0e5e30031.mjs"]
|
| 967 |
: extension === "epub" ? []
|
|
|
|
| 961 |
extension = (readerUrl.searchParams.get("ext") || "").toLowerCase();
|
| 962 |
sourceUrl = readerUrl.searchParams.get("url") || "";
|
| 963 |
} catch (_) { return; }
|
| 964 |
+
const shellAssets = ["/static/reader.css", "/static/reader-contract.js", "/static/reader-store.js", "/static/reader-request-manager.js", "/static/reader-chapter-repository.js", "/static/reader-scroll-anchor.js", "/static/reader-section-virtualizer.js", "/static/reader-runtime.js", "/static/reader-format-adapters.js", "/static/reader-security.js", "/static/reader.js"];
|
| 965 |
const engineAssets = extension === "pdf"
|
| 966 |
? ["/static/vendor/pdf.min.f80490490320.mjs", "/static/pdf-worker-wrapper.mjs", "/static/vendor/pdf.worker.min.8ab0e5e30031.mjs"]
|
| 967 |
: extension === "epub" ? []
|
static/reader-security.js
CHANGED
|
@@ -7,5 +7,6 @@
|
|
| 7 |
async function readText(response, limit, encoding = "utf-8") { return new TextDecoder(encoding).decode(await readBytes(response, limit)); }
|
| 8 |
function inspectZip(bytes, limits = LIMITS) { bytes = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes); if (bytes.byteLength > limits.archiveCompressedBytes) throw error("READER_ARCHIVE_LIMIT"); const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); let entries = 0, compressed = 0, expanded = 0; for (let offset = 0; offset + 46 <= bytes.byteLength;) { const signature = view.getUint32(offset, true); if (signature !== 0x02014b50) { offset += 1; continue; } const packed = view.getUint32(offset + 20, true), unpacked = view.getUint32(offset + 24, true), nameLength = view.getUint16(offset + 28, true), extraLength = view.getUint16(offset + 30, true), commentLength = view.getUint16(offset + 32, true); if (packed === 0xffffffff || unpacked === 0xffffffff) throw error("READER_ARCHIVE_LIMIT"); entries += 1; compressed += packed; expanded += unpacked; if (entries > limits.archiveEntries || expanded > limits.archiveExpandedBytes || (packed === 0 ? unpacked > 0 : unpacked / packed > limits.archiveRatio)) throw error("READER_ARCHIVE_LIMIT"); offset += 46 + nameLength + extraLength + commentLength; } if (!entries) throw error("READER_ARCHIVE_INVALID"); return Object.freeze({ entries, compressed, expanded }); }
|
| 9 |
function validateChapterManifest(manifest) { if (manifest?.version !== 1 || manifest?.kind !== "epub-chapters" || !Array.isArray(manifest.chapters) || !manifest.chapters.length) throw error("EPUB_INVALID"); if (manifest.chapters.length > LIMITS.chapters) throw error("READER_RESOURCE_LIMIT"); let declaredBytes = 0; for (const chapter of manifest.chapters) { const size = Number(chapter.bytes ?? chapter.size ?? 0); if (Number.isFinite(size) && size > 0) { if (size > LIMITS.chapterBytes) throw error("READER_RESOURCE_LIMIT"); declaredBytes += size; } } if (declaredBytes > LIMITS.chapterTotalBytes) throw error("READER_RESOURCE_LIMIT"); return manifest; }
|
| 10 |
-
|
|
|
|
| 11 |
})(typeof self !== "undefined" ? self : globalThis);
|
|
|
|
| 7 |
async function readText(response, limit, encoding = "utf-8") { return new TextDecoder(encoding).decode(await readBytes(response, limit)); }
|
| 8 |
function inspectZip(bytes, limits = LIMITS) { bytes = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes); if (bytes.byteLength > limits.archiveCompressedBytes) throw error("READER_ARCHIVE_LIMIT"); const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); let entries = 0, compressed = 0, expanded = 0; for (let offset = 0; offset + 46 <= bytes.byteLength;) { const signature = view.getUint32(offset, true); if (signature !== 0x02014b50) { offset += 1; continue; } const packed = view.getUint32(offset + 20, true), unpacked = view.getUint32(offset + 24, true), nameLength = view.getUint16(offset + 28, true), extraLength = view.getUint16(offset + 30, true), commentLength = view.getUint16(offset + 32, true); if (packed === 0xffffffff || unpacked === 0xffffffff) throw error("READER_ARCHIVE_LIMIT"); entries += 1; compressed += packed; expanded += unpacked; if (entries > limits.archiveEntries || expanded > limits.archiveExpandedBytes || (packed === 0 ? unpacked > 0 : unpacked / packed > limits.archiveRatio)) throw error("READER_ARCHIVE_LIMIT"); offset += 46 + nameLength + extraLength + commentLength; } if (!entries) throw error("READER_ARCHIVE_INVALID"); return Object.freeze({ entries, compressed, expanded }); }
|
| 9 |
function validateChapterManifest(manifest) { if (manifest?.version !== 1 || manifest?.kind !== "epub-chapters" || !Array.isArray(manifest.chapters) || !manifest.chapters.length) throw error("EPUB_INVALID"); if (manifest.chapters.length > LIMITS.chapters) throw error("READER_RESOURCE_LIMIT"); let declaredBytes = 0; for (const chapter of manifest.chapters) { const size = Number(chapter.bytes ?? chapter.size ?? 0); if (Number.isFinite(size) && size > 0) { if (size > LIMITS.chapterBytes) throw error("READER_RESOURCE_LIMIT"); declaredBytes += size; } } if (declaredBytes > LIMITS.chapterTotalBytes) throw error("READER_RESOURCE_LIMIT"); return manifest; }
|
| 10 |
+
function isZipContainer(extension) { return ["epub", "fbz"].includes(String(extension || "").toLowerCase()); }
|
| 11 |
+
root.VoiceOfMLReaderSecurity = Object.freeze({ LIMITS, assertResponseSize, readBytes, readText, inspectZip, validateChapterManifest, isZipContainer });
|
| 12 |
})(typeof self !== "undefined" ? self : globalThis);
|
static/reader.js
CHANGED
|
@@ -573,6 +573,7 @@ function loadScript(url) {
|
|
| 573 |
}
|
| 574 |
function fetchWithReaderTimeout(url, timeoutMs = READER_PROXY_TIMEOUT_MS) { return readerRequestManager.request(url, timeoutMs); }
|
| 575 |
function fetchReaderUrl(rawUrl) {
|
|
|
|
| 576 |
const proxyUrl = `/api/reader-content?url=${encodeURIComponent(rawUrl)}`;
|
| 577 |
return fetchWithReaderTimeout(proxyUrl).then((response) => response.ok ? response : fetchWithReaderTimeout(rawUrl), () => fetchWithReaderTimeout(rawUrl));
|
| 578 |
}
|
|
@@ -593,7 +594,7 @@ function fail(message, code = "READER_PARSE") { if (readerLifecycle.disposed) re
|
|
| 593 |
async function renderPdfPages(prepared) {
|
| 594 |
const response = await prepared; if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
| 595 |
const manifest = JSON.parse(await VoiceOfMLReaderSecurity.readText(response, VoiceOfMLReaderSecurity.LIMITS.manifestBytes)), entries = (manifest.pages || []).map((item) => ({ page: Number(item.page), path: String(item.path || "") })).sort((a, b) => a.page - b.page), rootMatch = entries[0]?.path.match(/^(objects\/[0-9a-f]{2}\/[0-9a-f]{64}\/[0-9a-f]{16})\/pages\//);
|
| 596 |
-
if (manifest.version !== 1 || manifest.kind !== "pdf-pages" || !rootMatch || !entries.length || entries.some((item, index) => item.page !== index + 1 || !new RegExp(`^
|
| 597 |
pdfPageManifest = { entries }; updateDocumentState({ pageCount: entries.length }); pageInput.max = String(documentState.pageCount); document.querySelector("#page-total").textContent = `/ ${documentState.pageCount}`; status.textContent = `${documentState.pageCount} 页`;
|
| 598 |
const visibleObserver = new IntersectionObserver((items) => {
|
| 599 |
const intersecting = items.filter((entry) => entry.isIntersecting).sort((a, b) => {
|
|
@@ -1089,7 +1090,7 @@ async function renderFoliate(prepared) {
|
|
| 1089 |
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
| 1090 |
await import("/static/foliate-reader/view.js");
|
| 1091 |
const bytes = await VoiceOfMLReaderSecurity.readBytes(response, VoiceOfMLReaderSecurity.LIMITS.archiveCompressedBytes);
|
| 1092 |
-
VoiceOfMLReaderSecurity.inspectZip(bytes);
|
| 1093 |
const view = document.createElement("foliate-view"),
|
| 1094 |
stream = document.createElement("div");
|
| 1095 |
view.className = "foliate-reader-view";
|
|
|
|
| 573 |
}
|
| 574 |
function fetchWithReaderTimeout(url, timeoutMs = READER_PROXY_TIMEOUT_MS) { return readerRequestManager.request(url, timeoutMs); }
|
| 575 |
function fetchReaderUrl(rawUrl) {
|
| 576 |
+
if (String(rawUrl).includes("/api/reader-bucket-resource?")) return fetchWithReaderTimeout(rawUrl);
|
| 577 |
const proxyUrl = `/api/reader-content?url=${encodeURIComponent(rawUrl)}`;
|
| 578 |
return fetchWithReaderTimeout(proxyUrl).then((response) => response.ok ? response : fetchWithReaderTimeout(rawUrl), () => fetchWithReaderTimeout(rawUrl));
|
| 579 |
}
|
|
|
|
| 594 |
async function renderPdfPages(prepared) {
|
| 595 |
const response = await prepared; if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
| 596 |
const manifest = JSON.parse(await VoiceOfMLReaderSecurity.readText(response, VoiceOfMLReaderSecurity.LIMITS.manifestBytes)), entries = (manifest.pages || []).map((item) => ({ page: Number(item.page), path: String(item.path || "") })).sort((a, b) => a.page - b.page), rootMatch = entries[0]?.path.match(/^(objects\/[0-9a-f]{2}\/[0-9a-f]{64}\/[0-9a-f]{16})\/pages\//);
|
| 597 |
+
if (manifest.version !== 1 || manifest.kind !== "pdf-pages" || !rootMatch || !entries.length || entries.some((item, index) => item.page !== index + 1 || !new RegExp(`^${rootMatch[1]}/pages/page-[0-9]{6}\\.webp$`).test(item.path))) throw new Error("PDF_MANIFEST_INVALID");
|
| 598 |
pdfPageManifest = { entries }; updateDocumentState({ pageCount: entries.length }); pageInput.max = String(documentState.pageCount); document.querySelector("#page-total").textContent = `/ ${documentState.pageCount}`; status.textContent = `${documentState.pageCount} 页`;
|
| 599 |
const visibleObserver = new IntersectionObserver((items) => {
|
| 600 |
const intersecting = items.filter((entry) => entry.isIntersecting).sort((a, b) => {
|
|
|
|
| 1090 |
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
| 1091 |
await import("/static/foliate-reader/view.js");
|
| 1092 |
const bytes = await VoiceOfMLReaderSecurity.readBytes(response, VoiceOfMLReaderSecurity.LIMITS.archiveCompressedBytes);
|
| 1093 |
+
if (VoiceOfMLReaderSecurity.isZipContainer(extension)) VoiceOfMLReaderSecurity.inspectZip(bytes);
|
| 1094 |
const view = document.createElement("foliate-view"),
|
| 1095 |
stream = document.createElement("div");
|
| 1096 |
view.className = "foliate-reader-view";
|
static/sw.js
CHANGED
|
@@ -11,6 +11,7 @@ const PRECACHE_URLS = [
|
|
| 11 |
"/static/reader-section-virtualizer.js",
|
| 12 |
"/static/reader-runtime.js",
|
| 13 |
"/static/reader-format-adapters.js",
|
|
|
|
| 14 |
"/static/reader.html",
|
| 15 |
"/static/reader.css",
|
| 16 |
"/static/reader.js",
|
|
@@ -33,6 +34,7 @@ const READER_RUNTIME_PATHS = new Set([
|
|
| 33 |
"/static/reader-section-virtualizer.js",
|
| 34 |
"/static/reader-runtime.js",
|
| 35 |
"/static/reader-format-adapters.js",
|
|
|
|
| 36 |
"/static/reader.css",
|
| 37 |
"/static/reader.js"
|
| 38 |
]);
|
|
|
|
| 11 |
"/static/reader-section-virtualizer.js",
|
| 12 |
"/static/reader-runtime.js",
|
| 13 |
"/static/reader-format-adapters.js",
|
| 14 |
+
"/static/reader-security.js",
|
| 15 |
"/static/reader.html",
|
| 16 |
"/static/reader.css",
|
| 17 |
"/static/reader.js",
|
|
|
|
| 34 |
"/static/reader-section-virtualizer.js",
|
| 35 |
"/static/reader-runtime.js",
|
| 36 |
"/static/reader-format-adapters.js",
|
| 37 |
+
"/static/reader-security.js",
|
| 38 |
"/static/reader.css",
|
| 39 |
"/static/reader.js"
|
| 40 |
]);
|