# 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.