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>
116 lines
4.8 KiB
Markdown
116 lines
4.8 KiB
Markdown
# Architektura
|
|
|
|
## Struktura projektu
|
|
|
|
```text
|
|
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
|
|
|
|
1. Nacteni zdroje. U `source.url` se dokument stahne, kazdy hop presmerovani
|
|
projde SSRF kontrolou. U `source.html` se pouzije telo requestu.
|
|
2. Volba enginu. Rezim `auto` hleda aktivni skripty, jinak bere WeasyPrint.
|
|
3. Volba rezimu cislovani stranek. Musi padnout pred injektazi stylu, protoze
|
|
styl se lisi podle toho, jestli cisla resi CSS countery nebo cislovaci vrstva.
|
|
4. Sestaveni dokumentu. Doplni se `base`, prida se styl stranky, nadpisy dostanou
|
|
`id` a pokud je zapnuty obsah, vlozi se jeho zastupna verze.
|
|
5. Deleni na casti.
|
|
6. Prvni pruchod renderu. Vysledkem jsou hotova PDF casti, jejich pocty stranek
|
|
a pozice kotev.
|
|
7. Druhy pruchod, pokud se generuje obsah. Zastupna cisla se nahradi skutecnymi.
|
|
8. Slouceni casti.
|
|
9. 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_JOBS` jobech, 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 `WORKERS` jobu, 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_SECONDS` smazou 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 `http` a `https`.
|
|
- 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.
|