Štítek: bezpečnost

Kdo se moc ptá, moc se dozví

Po vší té práci na blogu jsem chtěl vědět, co by šlo na blogu zrychlit, co vyhladit, co utěsnit, a tak jsem se na to zeptal Fable - nejnovějšího LLM modelu Claude od
Anthropic. Čekal jsem, že mi dá pár nápadů na později a dostal jsem osm commitů, přepsaný velký kus architektury a opravu bugu, o kterém jsem ani netušil.

Půlka archivu, o které jsem nevěděl

První věc, na kterou analýza narazila, byla, že content/posts/ mělo najednou 5 822 souborů místo 3 274. iCloud si při nějaké synchronizaci sám pro sebe vyrobil 2 548 kopií — se jménem <slug> 2.json, macOSí konvencí pro konfliktní soubor. Build by to tiše sežral a zdvojil by se mi celý archiv, výpisy, RSS, sitemapa i vyhledávání.

Server je jediný autoritativní zdroj, tak jsem to neopravoval, lokální kopii prostě zahodil a stáhl znovu — rsync --checksum --delete. Vedlejší bonus: několik nových a dva upravené posty se mi nově stáhlo přímo ze serveru.

Aby se tohle příště nestalo, přidal jsem do buildu pojistku: když dva posty vyjdou na stejný rok a slug, celý build spadne s jasnou hláškou místo tichého zdvojení. A do nahrávání na server zase zrcadlovou pojistku — když se počet souborů k nahrání najednou zvýší o víc než pětinu, deploy se zastaví. Dřív hlídal jen prudký úbytek (rozbitý build), ne prudký nárůst.

Fonty, hlavičky a jeden zapomenutý roh archivu

Web dosud tahal Open Sans z Google Fonts — cizí origin do kritické cesty vykreslení a IP každého návštěvníka mířící rovnou Googlu. Stáhl jsem si font k sobě a díky tomu zjistil, že aktuální verze Open Sans na Google Fonts je variabilní font, takže tři váhy (400/600/700) sedí v jediném souboru na jazykový subset a budu mít dva soubory místo šesti.

Po fontech přišla na řadu Content-Security-Policy, a ta odhalila něco 29 starších postů z migrace s vloženým syrovým embed videem z youtube.com, ne z bezpečnějšího youtube-nocookie.com, jak to dělají nová videa. CSP musela počítat s oběma, jinak by starým videím zčerstva přestalo fungovat přehrávání.

Dva posty také táhly skript z platform.instagram.com — čistý trackovací kód z dob migrace. Ten jsem z dat vystřihl úplně a nahradil obyčejnou kartou s odkazem.

Jednu díru jsem naopak zavřel jen na papíře, protože se zavřít nedá: Surfer, kam se web nahrává, bere přihlašovací token jako součást URL, takže končí v access logu. Ověřil jsem, že token přes hlavičku Authorization prostě na Surfer nejde poslat, a u nahrávání souborů (naprostá většina provozu) by ani tělo požadavku nepomohlo. Zůstává to tedy tak, jak to je, se zdůvodněním rovnou v kódu, ať se za k tomu za rok nevracím se stejnou otázkou.

.nosync, aneb jak nesyncovat, co nepotřebuju

Lokální public/ jsem měl na Macu v iCloud Drive — každý build tak přepisoval stovky megabajtů, co iCloud hned začal poctivě nahrávat pryč, přestože jde o čistě odvozený výstup, který jde kdykoli
znovu "vyrobit". Přejmenování na public.nosync iCloud ze sledování vyřadí. Je to nedokumentované chování, ale ověřil jsem si ho přímo: xattr na přejmenovaném adresáři ukáže
com.apple.fileprovider.dir#N, kterou synchronizovaný adresář nemá.

Stejná úvaha platila i pro content/ a media/ — takže i tyhle dva adresáře dostaly .nosync a lokální build, který ještě ráno neproběhl ani za deset minut kvůli iCloudí I/O režii, teď proběhne
za deset vteřin.

Vyhledávání, které se učilo za pochodu

Index pro vyhledávání rostl s každým postem a stahoval se celý na každou návštěvu /search/ — 488 kB komprimovaně, i když návštěvník hledal něco z minulého týdne. Rozdělil jsem ho na dva: pět set nejnovějších postů se stáhne hned, zbytek archivu líně, až někdo skutečně něco zadá do políčka.

Cheat sheet přestal být rukojmím jednoho postu

Nápověda pro markdown syntaxi žila jako obyčejný publikovaný post, a odkaz na ni v editoru byl natvrdo zadrátovaný na jeho adresu. Kdybych ho někdy smazal nebo odpublikoval, nápověda by mířila do prázdna — nikdo by o tom nevěděl, dokud by na to nekliknul.

Vytáhl jsem markdown parser (270 řádků, co dosud žily zavřené v nástroji pro psaní postů) do vlastní knihovny a naučil build stavět i stránky mimo content/posts/. Cheat sheet je teď /markdown/ — generuje se přímo ze souboru v repozitáři při každém buildu, stejně jako se dnes generuje vyhledávání. Nejde ho smazat ani odpublikovat, protože to není post. Existuje, dokud existuje ten soubor.

Jak to dopadlo

  • lokální build: z přes 10 minut na 10 vteřin
  • soubory k hashování při běžném nasazení: ze všech ~7 700 na jen změněné
  • search-index.json na každou návštěvu vyhledávání: z 488 kB na 137 kB, zbytek líně
  • requesty na Mastodon při běhu cronu statistik: ze všech tootovaných postů na ty z jen posledních 90 dní (starší jednou týdně)
  • CSP hlavička, self-hosted fonty, žádný trackovací skript v datech: 0 → hotovo
  • osm commitů, 18 souborů, skoro devět set přidaných řádků — a jedna oprava bugu, o kterém jsem ještě ráno nevěděl

Chtěl jsem krátký přehled, ale Fable mi dal kompletní analýzu včetně řešení a oprav.

Číst dál

Když změna jednoho řádku CSS znamená nahrát šest tisíc souborů

Chtěl jsem tu změnit barvu odkazů. Jeden řádek ve stylech. Spustil jsem deployment a s hrůzou v očích koukal, jak se na server nahrává šest tisíc souborů.

Nic se nerozbilo, ale bylo jasné, že něco je velký špatný.

Může za to vychytávka

Každá stránka měla v hlavičce otisk obsahu stylů:

<link rel="stylesheet" href="/assets/css/site.css?v=a3f9c21b04">

Když se styly změní, změní se adresa a prohlížeč si stáhne novou verzi místo té nacachované. Dal jsem to tam schválně a byl na to docela pyšný.

Jenže se tím otisk propsal do každé stránky webu. Změním jeden znak v CSS, změní se všech 6 228 stránek — a deploy skript, který umí posílat jen to, co se skutečně změnilo, to poslušně pošle všechno.

Když jsem se podíval, co vlastně moje úložiště posílá prohlížečům, došlo mi to:

$ curl -sI https://sean.cz/assets/css/site.css
cache-control: public, max-age=0
etag: W/"409e-19f9a201251"

max-age=0 znamená, že se prohlížeč u každého načtení stejně ptá, jestli se soubor nezměnil. Ta vychytávka nebyla nikdy potřeba. Řešila problém, který jsem neměl, a způsobila ten, co jsem měl.

Nový příspěvek přepsal celý archiv

Stránky výpisu se stránkovaly odshora: deset nejnovějších na první stranu, dalších deset na druhou. Nový příspěvek se zařadí na začátek a posune všechno ostatní — hranice všech 328 stran se posunou s ním. Nový post tak znamenal přepsat cca 435 souborů.

Spravilo se to obrácením směru. Desítky se teď počítají od nejstaršího příspěvku, takže nejstarší desítka je navždycky stejná a nový text se přidá jen do té poslední, rozepsané stránky.

Daň je, že /page/1/ dneska znamená nejstarší příspěvky, ale to nevadí, protože se na ně chodí přes „Novější / Starší".

Věc, kterou jsem nečekal

Při ověřování jsem přidal zkušební příspěvek a čekal devět změněných souborů. Vyšlo jich 65 — a byly mezi nimi stránky, které s tím textem nemohly mít nic společného.

Ukázalo se, že když web přegeneruju dvakrát za sebou, pokaždé vyleze trochu jiný výsledek. Příspěvky se řadí podle data a Ruby negarantuje, který ze dvou se stejným datem skončí první. A po stěhování z Tumblru a Twitteru má 110 příspěvků úplně stejné datum jako nějaký jiný.

Nic nezměním, spustím build a přesto mám změněné soubory. Vůbec jsem to netušil. Oprava je jeden řádek: když je datum stejné, rozhodne jméno příspěvku.

Měřit, ne hádat

Byl jsem si jistý, co bude nejdražší operací — kopírování 384 MB fotek. Změřil jsem to a představa se rozsypala. Fotky: jedna sekunda.

Nejdražší bylo mazání složky s hotovým webem, 3,8 sekundy. Build ji vždycky smazal celou a hned do ní nasypal zpátky skoro totožných 7 700 souborů. Teď se nemaže; zapisuje se jen to, co se opravdu liší.

Dvě věci mimo rychlost

Komentáře pod články tahám z Mastodonu a jméno autora jsem vkládal do stránky bez ošetření. Jenže jméno účtu si nastavuje kdokoli ve Fediverse — stačilo se přejmenovat na kus kódu, odpovědět mi na toot a nechat si ho spustit v prohlížeči každému čtenáři. Učebnicová díra. Zavřeno.

A postranní panel si tahal poslední tooty a commity přímo v prohlížeči návštěvníka: čtyři a víc dotazů na cizí služby při každém otevření jakékoli stránky. GitHub navíc nepřihlášené pouští jen šedesátkrát za hodinu, takže ta kartička po chvíli mizela. Teď si data stáhne server.

Jak to dopadlo

  • změna stylů: z 6 228 souborů na jeden
  • nový příspěvek: z ~435 na sedm až devět
  • přegenerování beze změny obsahu: z desítek tiše změněných souborů na nulu
  • dotazy na cizí služby při otevření stránky: ze čtyř a víc na žádný
  • obrázky stažené na úvodní stránce: z desítek na jeden
  • build: ze třinácti sekund na sedm a půl

Dvě poučení. Byl jsem si jistý, že problém jsou fotky, a byla to složka, na kterou bych nesáhl ani po dlouhém přemýšlení. A to prohazování pořadí nikdy nespadlo, nic nenahlásilo, nic nerozbilo — jen potichu dělalo práci navíc.


Jestli jste četli Jak je postavený tenhle blog, tak ho tenhle článek na pár místech přepisuje — tooty ani commity už se v prohlížeči netahají a složka s hotovým webem se nemaže. Původní text nechávám být; popisoval stav, který tehdy platil.

Číst dál