Sluzba prijme adresu HTML dokumentu nebo HTML v tele requestu a vrati PDF. Navrzena pro dokumenty o stovkach az tisicich stranek. Rendering: - WeasyPrint jako vychozi engine, spravne CSS Paged Media, nizka pametova narocnost, bez JavaScriptu - Chromium pres Playwright pro dokumenty dokreslovane skripty - rezim auto s detekci skriptu a fallbackem pri selhani WeasyPrintu Velke dokumenty: - deleni na casti na strukturalnich hranicich, rez nikdy uvnitr tabulky nebo odstavce - dvoupruchodovy render obsahu se skutecnymi cisly stranek, pozice nadpisu se ctou z kotev hlasenych u kazde stranky - cislovani stranek bud pres CSS countery, nebo pres cislovaci vrstvu nastampovanou na hotove PDF, rozmer stranky se cte z vysledneho souboru - Chromium se restartuje po N jobech, nikdy vsak behem beziciho renderu API: - POST /convert synchronne, POST /jobs asynchronne se sledovanim stavu, stahovanim vysledku, rusenim a volitelnym callbackem - GET /health s overenim dostupnosti obou enginu a stavem fronty - OpenAPI respektuje prefix reverse proxy pres root_path Bezpecnost a provoz: - SSRF kontrola po DNS resolvu, na kazdem presmerovani a u vsech pozadavku prohlizece - nedostupne assety render nezastavi, ale hlasi se v odpovedi i v logu - fronta s omezenym poctem workeru, rozpracovane joby se pri ukonceni oznaci jako failed, nezmizi potichu - strukturovane JSON logovani s job_id - vsechny limity vypnute ve vychozim stavu Dockerfile je dvoufazovy, obsahuje zavislosti WeasyPrintu, Chromium a fonty s ceskou diakritikou. Autentizace zamerne neni implementovana, zpusob predavani neni domluveny. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4.8 KiB
Architektura
Struktura projektu
app/
main.py vytvoreni aplikace, lifespan, obsluha chyb
config.py konfigurace z environment variables
models.py Pydantic modely requestu a odpovedi
errors.py typove chyby a mapovani na HTTP kody
logging_setup.py strukturovane JSON logovani
deps.py kontejner sluzeb sestaveny pri startu
routers/
health.py /health a /version
convert.py synchronni /convert
jobs.py asynchronni /jobs
services/
security.py SSRF kontroly, resolv adres
fetcher.py stahovani dokumentu a assetu, hlaseni vypadku
pipeline.py cely prubeh konverze
jobs.py fronta jobu a workeri
storage.py docasne ulozeni vysledku
engines/
base.py spolecne rozhrani enginu
weasy.py WeasyPrint
chromium.py Playwright a headless Chromium
pdf/
document.py parsovani, nadpisy, obsah
chunker.py deleni HTML na casti
merger.py slucovani PDF
paginator.py cislovaci vrstva
styles.py generovani stylu stranky
Prubeh konverze
- Nacteni zdroje. U
source.urlse dokument stahne, kazdy hop presmerovani projde SSRF kontrolou. Usource.htmlse pouzije telo requestu. - Volba enginu. Rezim
autohleda aktivni skripty, jinak bere WeasyPrint. - Volba rezimu cislovani stranek. Musi padnout pred injektazi stylu, protoze styl se lisi podle toho, jestli cisla resi CSS countery nebo cislovaci vrstva.
- Sestaveni dokumentu. Doplni se
base, prida se styl stranky, nadpisy dostanouida pokud je zapnuty obsah, vlozi se jeho zastupna verze. - Deleni na casti.
- Prvni pruchod renderu. Vysledkem jsou hotova PDF casti, jejich pocty stranek a pozice kotev.
- Druhy pruchod, pokud se generuje obsah. Zastupna cisla se nahradi skutecnymi.
- Slouceni casti.
- Nastampovani cislovaci vrstvy, pokud cisla neresi CSS countery.
Proc dvoupruchodovy render
Cisla stranek v obsahu nejsou znama drive, nez se dokument vyrenderuje. Zaroven plati, ze vlozeni obsahu posune cisla stranek, ktera obsah uvadi.
Reseni je vlozit obsah uz v prvnim pruchodu, jen s pevne sirokou vyplni misto cisel. Rozvrzeni je proto v obou pruchodech stejne a cisla zjistena v prvnim pruchodu plati i po druhem. Cislo stranky je v tabulce zarovnane doprava ve sloupci pevne sirky, takze ani jiny pocet cislic rozvrzeni nezmeni.
Pozice nadpisu se ctou z kotev, ktere WeasyPrint hlasi u kazde stranky. To je presnejsi nez odhad z poradi zalozek.
Proc se cisla stranek u clenenych dokumentu dopisuji az nakonec
CSS counter page se v kazde renderovane casti restartuje od jednicky a
counter(pages) zna jen pocet stranek dane casti. Ve sloucenem dokumentu by
proto cislovani bylo nesmyslne.
Cislovaci vrstva je samostatny dokument o stejnem poctu stranek a stejnem
rozmeru, ktery obsahuje jen cisla v okraji. Ten se pres hotove PDF nastampuje
stranku po strance. Rozmer stranky se cte z prvni stranky vysledneho PDF, takze
vrstva sedi i kdyz si dokument nastavil vlastni @page size.
Fonty vrstvy kresli WeasyPrint a vklada je do souboru, vysledek proto nezavisi na fontech v systemu, ktery PDF otevira.
Pamet
Kriticky bod u tisicistrankovych dokumentu.
- Render probiha po castech, v pameti je vzdy jen jedna cast.
- Chromium se restartuje po
CHROMIUM_RESTART_AFTER_JOBSjobech, protoze postupne unika pamet. - Slucovani pouziva pypdf, ktere drzi stranky vysledku v pameti. U velmi velkych
dokumentu je to nejnarocnejsi krok cele konverze. V image je nainstalovany
i
qpdf, ktery by slo pouzit jako nahradu, pokud by pamet prestala stacit. - Vysledek se ke klientovi streamuje, nenacita se cely do pameti.
Fronta
Fronta je v pameti procesu. Zadna databaze, zadny broker.
- Soubezne bezi
WORKERSjobu, ostatni cekaji ve fronte. - Plna fronta vraci 503 s kodem
queue_full. - Pri ukonceni sluzby se rozpracovane joby oznaci jako failed s kodem
service_restarted. Nezmizi potichu. - Vysledky se po
JOB_RESULT_TTL_SECONDSsmazou a job se odstrani. - Pri startu se smazou adresare jobu, ktere po restartu zustaly bez zaznamu.
Bezpecnost
Sluzba na pozadani stahuje libovolnou adresu, coz je presne tvar SSRF zranitelnosti.
- Povolena jsou jen schemata
httpahttps. - Kontrola probiha az po DNS resolvu, takze verejna domena mirici na 127.0.0.1 neprojde.
- Kontrola se opakuje na kazdem presmerovani.
- Blokovane jsou loopback, privatni rozsahy, link local vcetne 169.254.169.254, CGNAT, multicast a rezervovane rozsahy.
- U Chromia jde kazdy pozadavek prohlizece pres
context.route, takze stejnou kontrolou projdou i assety, ktere si stranka dotahne sama. - Seznam blokovanych rozsahu jde rozsirit i zuzit konfiguraci. Vychozi stav je blokovat.