Repository navigation
Conversation
Two read-only routes, GET /api/sources/preview/chapters and /api/sources/preview/pages. Nothing is persisted -- no series row, no files, no progress -- and page images are rendered through the existing /img/sources/cover proxy rather than a new byte proxy, so the SSRF guard, the source's referer and Cloudflare session, and the /img/ authorization hook all apply unchanged. Extension sources are refused for pages (422 preview_unsupported) before the engine is asked: their page URLs are on the engine's private origin, which the proxy only fetches thumbnails from, so every page would have been a broken image. Both routes are in openapi.yaml and docs/api.md, and sourceAccess.int.test.ts covers them: canDownload and the age cap refuse them like every other route in the plugin, the list is one copy per number, nothing is written, and the extension case never reaches the adapter. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Whether a title is worth keeping is usually one chapter's worth of question. Answering it by adding, reading and removing left a folder, a listing, a floor and a row behind. The preview writes nothing: chapters and page URLs come from the source on demand and render through the existing source-image proxy, at its widest whitelisted width (1600). Not the real reader, deliberately -- that one is built on a book id and carries progress, bookmarks, offline copies and the next-chapter chain, none of which exist for something outside the library. Not offered for extension sources, whose pages the proxy cannot fetch. The new strings are translated in all eight locales, and USAGE.md describes the button. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…re solver GET /img/sources/cover takes its URL from the request and is open to any signed-in reader. For a source marked requiresCloudflare it handed that URL to cfSession -- FlareSolverr's browser, on the same Docker network as the extension engine and the database -- BEFORE the DNS half of the SSRF guard ran; the check that did run first knew literal private addresses and four suffixes, not bare container names. So an account could make the solver open uchiyomi-suwayomi, yomi-db, or any public name pointed at a private address. Blind and time-capped, but a browser follows redirects and runs the page's scripts. - assertPublicHost runs before anything touches the network, the solver included (images.ts). - isBlockedHost refuses a hostname with no dot, and a trailing dot names the same host (ssrfGuard.ts). It was deliberately left to DNS; that held only while nothing reached the network first. - A caller-supplied URL reaches the solver only on a host the source vouched for: its own admin-configured site, or a host it has served covers from, learned where every adapter enters (imageHosts.ts, loader.ts). Library covers come from series_art and keep the solver: Aqua's covers sit on a Cloudflare CDN that answers 403 without clearance (measured). coverSolverGuard.test.ts records every URL a fake FlareSolverr is asked to open; each fix was proved by undoing it alone and watching its named assertion fail. Full bff suite: 1867/1867. Found while reviewing #91. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oading Read a chapter before adding a series (#91), a source order for new chapters (#93, first half), swapping in a preferred group's copy once it exists (#93, second half; closes #81), chapter names borrowed from a source that numbers the work the same way (#85) and a compact chapter list (#88) -- @Squeaks72's ideas, rebuilt where the originals could not be merged. Every running download, including the server's own sweeps, repairs and bulk fetches, now shows with a Cancel (#82). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Squeaks72's preview: under Add to library, "Read a chapter first" opens the title straight from the source, pick a chapter, scroll it, step to the next, and Add when decided. Nothing is written. Rebuilt because the first version passed the caller's chapterId to the source's getPageUrls, which for the site engines is cfGet(chapterId): FlareSolverr's browser on the Docker network visiting whatever a signed-in account named, the hole v0.45.1 closed in the cover proxy. Now: - a chapter is named by its NUMBER in the listing the server fetched for that series (the add dialog's cached detail); the page by its INDEX. No URL and no chapter id from the client reaches a source, and the list sent to the client carries neither; - pages are fetched by the server through the guarded image fetcher (DNS check first, redirects re-checked; an extension's page only from the engine's own origin), one at a time per source, and served as the original bytes, no-store, only when they are an image, at most 15 MB -- not through /img/sources/cover, which cached a year of pages for series nobody added; - refused for an account with an age limit (a preview reads a site before any library's rating applies), without the download permission, and for a disabled or cooling-down source; the messages are generic; - the viewer is portalled to <body> (the dialog's backdrop-filter squeezed a fixed viewer into its panel) and takes Escape in the capture phase, so one Escape goes back one step instead of closing the add dialog too. Every rule was proved by reintroducing its bug, the first version's chapterId route included (bff/test/preview.int.test.ts). Co-authored-by: Squeaks72 <Squeaks72@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit ba4903ddc46209d8740556f9f89bc91a321504a7)
|
Thanks for this, previewing a chapter before you add a series is a great idea. I can't merge it as it is, though. The new pages route passes the caller's There were some smaller things as well: the age cap never applied to add-a-site sources, I rebuilt it so a page can only be fetched for a chapter that's actually in that series' listing: the client names a chapter by its number and a page by its index, so no address from the browser reaches a source at all. The server fetches each page itself, through the same guard covers go through. One upside of doing it that way: extension sources work too, which yours had to leave out. Your UI was the starting point. It shipped in v0.47.0 as Read a chapter first in the add dialog, with credit to you. |
Summary
Deciding whether a title is worth keeping usually takes one chapter. Before this PR, the only way to read that chapter was to add the series, read it, and remove it again. That leaves a folder, a listing, a chapter floor and a row behind. This PR adds a Read a chapter first button to the add dialog. It opens the title straight from the source and writes nothing.
Server (
bff/src/routes/sources.ts): two read-only routes in the sources plugin.GET /api/sources/preview/chapters?source=&sourceId=lists the chapters with one copy per number, chosen by the samechooseReleases/effectivePrefsForrule that/api/sources/detailcounts with, so the list matches what an add would land.detailstays count-only on purpose, so the add dialog doesn't pay for rows it never reads.GET /api/sources/preview/pages?source=&chapterId=returns the source's own page URLs.canDownloadpreHandler and apply the same by-id age-cap rule asdetail: the cap answers 403, and the 18+ reveal does not apply.No new byte proxy. The web app renders each page through the existing
/img/sources/coverproxy. That proxy already fetches a remote image named by a source with the source's referer and Cloudflare cookies, behind the SSRF guard (fetchableCoverUrland the engine-origin exemption) and the root/img/authorization hook. A second proxy for pages would have to re-solve all of that, and the note onauthorizeImageRequestinroutes/images.tsrecords a downstream proxy that shipped without auth. One consequence: preview pages go into the same LRU image cache as covers (srccover2:1600:<url>).Things I found and fixed while porting this:
w=1400, but the proxy only allows 400, 800 and 1600 and treats anything else as 400. Every page rendered at cover-tile size. It now asks forw=1600(withoutEnlargementkeeps a narrower original as it is).engineCoverUrl). Widening that exemption to pages would give any signed-in reader an authenticated GET on the engine. So/preview/pagesanswers422 preview_unsupportedfor asw:source before it asks the engine anything, and the dialog doesn't offer the button for one. Built-in and add-a-site sources, whose pages are public URLs, work.sourceBrowsableForhelper that doesn't exist upstream) stacked above the chapters route.detail's own one-line comment had also been separated from its route. Each comment now sits on its own route.Web (
web/components/PreviewReader.tsx,AddSeriesDialog.tsx): this is not the real reader, on purpose. That reader is built on a book id and carries progress, bookmarks, offline copies and the next-chapter chain, and none of those exist for something outside the library. The preview is a chapter list, then a lazy-loaded vertical scroll with Previous and Next buttons, and Add to library at the bottom of the list.Desktop: the preview is available there too. Upstream keeps every
/api/sources/*route on desktop (Discover and adding work the same as on a server;DESKTOP_HIDDEN_ROUTEScovers only sign-in, members, sessions, tokens and push), and this is the same kind of feature.desktopRoutes.test.tspasses: its server table isopenapi.yaml, which now describes both routes.Type
Testing
Re-run after rebasing onto v0.45.0 (91b41aa): bff 1859 tests, 1846 pass, 9 fail, 4 skipped — the same 9 embedded-Postgres entrypoint failures
mainhas in this environment (main: 1857 / 1844 / 9 / 4), nothing new; web 431/431; both typechecks OK. The detail below is from the first run, on v0.44.0.I ran the upstream CI setup in
node:24-alpineagainst a realpostgres:16-alpine(TEST_DATABASE_URLset, so the SQL tests run instead of skipping), first on plainupstream/main(a8bc502) to get a baseline and then on this branch.tsc --noEmittsc --noEmitThe 9 bff failures are the same on both: they are the embedded-Postgres entrypoint tests (
not ok 291-299), which need an initdb/socket setup this sandbox doesn't have. No new failures.Added to
bff/test/sourceAccess.int.test.ts:ROUTEStable, socanDownload:falsehas to be refused on them like every other route in the pluginlib_seriesrowpreview_unsupported, and the adapter'sgetPageUrlsis never calledBoth routes are in
bff/openapi.yamland in the route list indocs/api.md, soopenapiCoverageanddesktopRoutesboth pass.The six new strings are translated in all eight locale files. The Add to library button reuses the existing key.
npm run test:i18nis a Puppeteer walk against a running server, so it could not run in this sandbox. I checked separately that everytr()key this PR adds is present in all eight files.Checklist
docs/USAGE.md)🤖 Generated with Claude Code