Renderowanie na brzegu sieci z Cloudflare i lokalnym mechanizmem awaryjnym
W skrócie
Dział zatytułowany „W skrócie”Mostek Cloudflare wysyła kod HTML do punktu końcowego renderowania Cloudflare Worker i zwraca PDF. Renderowanie odbywa się na brzegu sieci, dzięki czemu nie musisz utrzymywać długo działającego procesu przeglądarki. Tworzysz konfigurację opartą wyłącznie na HTTPS, podłączasz klienta zgodnego z PHP Standards Recommendation (PSR)-18 oraz fabryki PSR-17, wywołujesz render() i możesz dodać lokalny renderer na wypadek, gdy Worker jest nieosiągalny. Ten przewodnik pokazuje sposób wywołania renderowania, ścieżkę awaryjną, mechanizmy ochrony przed Server-Side Request Forgery (SSRF) i DNS-rebinding (przeadresowaniem DNS, Domain Name System) oraz przypinanie klucza publicznego Transport Layer Security (TLS), które mostek wymusza, zanim jakiekolwiek żądanie opuści proces.
Na początek spełnij wymagania wstępne:
- Rdzeń NextPDF oraz
nextpdf/cloudflaresą zainstalowane. - Punkt końcowy Cloudflare Worker udostępnia kontrakt renderowania przez HTTPS i akceptuje token typu bearer. Mostek odrzuca adres URL Worker spoza HTTPS, zanim cokolwiek wyśle.
- Dostępny jest klient PSR-18 (na przykład Guzzle 7) oraz fabryki żądań i strumieni PSR-17. W przypadku transportu cURL z przypinaniem zapewnij dodatkowo fabrykę odpowiedzi PSR-17 oraz
ext-curl. - Na potrzeby lokalnego mechanizmu awaryjnego dostępny jest
nextpdf/artisan(lub inny lokalny renderer).
To przewodnik praktyczny. Aby wykonać pierwsze działające renderowanie, zacznij od przewodnika szybkiego startu Cloudflare.
Instalacja
Dział zatytułowany „Instalacja”Zainstaluj mostek, klienta PSR-18 oraz fabryki PSR-17.
composer require nextpdf/cloudflare guzzlehttp/guzzleNa potrzeby lokalnego mechanizmu awaryjnego zainstaluj lokalny renderer, który mostek może wywołać.
composer require nextpdf/artisanWczytaj token bearer dla Worker oraz wszelkie dane uwierzytelniające R2 ze zmiennych środowiskowych lub z menedżera sekretów. Nigdy ich nie commituj.
Przegląd koncepcyjny
Dział zatytułowany „Przegląd koncepcyjny”CloudflareHtmlRenderer::render() waliduje kod HTML i miejsce docelowe, wysyła uwierzytelnione POST do Worker i parsuje odpowiedź. Worker zwraca surowe bajty PDF (Content-Type: application/pdf) lub treść JSON z polem pdf zakodowanym w base64. Renderer mapuje odpowiedź na final readonly CloudflareRenderResult, który zawiera bajty, żądaną szerokość, wysokość, lokalizację renderowania (wyprowadzoną z nagłówka CF-Ray) oraz czas renderowania.
Mostek rozdziela błędy na dwie jasno określone klasy:
CloudflareRenderException— Worker odpowiedział, ale renderowanie się nie powiodło (błąd HTTP lub treść, która nie zaczyna się od%PDF). Jest to błąd renderowania i nigdy nie jest ponawiany z użyciem mechanizmu awaryjnego.CloudflareNotAvailableException— nie udało się połączyć z brzegiem sieci i nie był dostępny żaden użyteczny mechanizm awaryjny.
Lokalny mechanizm awaryjny obsługuje drugi przypadek. Gdy nie można połączyć się z Worker, a fallbackToLocal ma wartość true, mostek wywołuje dostarczony przez Ciebie LocalRendererFactoryInterface. Robi to leniwie: metoda create() fabryki jest wywoływana tylko na ścieżce awaryjnej. W renderowaniu awaryjnym wartość renderLocation wyniku to dosłowny ciąg znaków local.
Mostek zabezpiecza granicę sieci, zanim jakiekolwiek żądanie opuści PHP. Odrzuca adres URL Worker spoza HTTPS. Odrzuca hosta Worker, który rozwiązuje się do prywatnej lub zarezerwowanej przestrzeni adresowej, sprawdzając wszystkie rekordy A i AAAA, a nie tylko pierwszy. Ponownie rozwiązuje też hosta bezpośrednio przed połączeniem, co zamyka okno time-of-check/time-of-use (TOCTOU) wobec DNS-rebinding. Gdy dostarczysz fabrykę odpowiedzi PSR-17 oraz albo rozwiązany zestaw adresów IP, albo przypięcia Subject Public Key Info (SPKI), mostek używa transportu cURL z przypinaniem. Ten transport wiąże połączenie ze zweryfikowanymi adresami IP (CURLOPT_RESOLVE), wymusza przypinanie klucza publicznego TLS (CURLOPT_PINNEDPUBLICKEY), weryfikuje partnera i hosta oraz nie podąża za przekierowaniami.
Powierzchnia API
Dział zatytułowany „Powierzchnia API”// Configuration (final readonly):new CloudflareRendererConfig( string $workerUrl, // required, must be HTTPS string $apiToken, // required, #[SensitiveParameter] int $renderTimeout = 30, string $defaultCss = '', int $maxHtmlSize = 5_000_000, ?string $r2FontBucket = null, bool $fallbackToLocal = true, list<string> $pinnedPublicKeys = [], // sha256/<base64> list<string> $backupPublicKeys = [],)CloudflareRendererConfig::fromArray(array $config): self
// The renderer:new CloudflareHtmlRenderer( CloudflareRendererConfig $config, ClientInterface $httpClient, // PSR-18 RequestFactoryInterface $requestFactory, // PSR-17 StreamFactoryInterface $streamFactory, // PSR-17 ?LoggerInterface $logger = null, // PSR-3 ?LocalRendererFactoryInterface $localRendererFactory = null, ?HtmlSecurityPolicyInterface $htmlSecurityPolicy = null, ?ResponseFactoryInterface $responseFactory = null, // enables pinned transport)CloudflareHtmlRenderer::render(string $html, float $widthPt = 595.28, float $heightPt = 0.0, list<string> $fontFiles = []): CloudflareRenderResultCloudflareHtmlRenderer::isAvailable(): boolrender() domyślnie przyjmuje szerokość A4 (595.28 punktów) oraz automatycznie wykrywaną wysokość (heightPt: 0). Pełny opis pól oraz mapowanie kluczy fromArray() znajdziesz na stronie konfiguracji Cloudflare w sekcji Zobacz także.
Przykład kodu — Szybki start
Dział zatytułowany „Przykład kodu — Szybki start”Utwórz konfigurację, zbuduj renderer, wykonaj renderowanie i zapisz bajty.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;use GuzzleHttp\Psr7\HttpFactory;use NextPDF\Cloudflare\CloudflareHtmlRenderer;use NextPDF\Cloudflare\CloudflareRendererConfig;use NextPDF\Cloudflare\Exception\CloudflareNotAvailableException;use NextPDF\Cloudflare\Exception\CloudflareRenderException;
$config = new CloudflareRendererConfig( workerUrl: 'https://pdf-renderer.example.workers.dev/render', apiToken: getenv('CF_PDF_TOKEN') ?: throw new RuntimeException('CF_PDF_TOKEN not set'),);
$httpFactory = new HttpFactory();
$renderer = new CloudflareHtmlRenderer( config: $config, httpClient: new Client(), requestFactory: $httpFactory, streamFactory: $httpFactory, responseFactory: $httpFactory, // enables the pinned cURL transport);
try { $result = $renderer->render('<h1>Hello from the edge</h1>');
if (!$result->isValid()) { throw new RuntimeException('Worker did not return a valid PDF'); }
file_put_contents('output.pdf', $result->pdfData);} catch (CloudflareRenderException $exception) { // Worker answered but the render failed. Not retried with fallback. fwrite(STDERR, 'Render failed: ' . $exception->getMessage() . PHP_EOL); exit(1);} catch (CloudflareNotAvailableException $exception) { // Edge unreachable and no usable fallback. fwrite(STDERR, 'Edge unavailable: ' . $exception->getMessage() . PHP_EOL); exit(2);}Token pochodzi ze środowiska i nigdy nie jest trwale zapisany w kodzie. workerUrl musi używać HTTPS; mostek odrzuca adres URL http://, zanim wyśle jakiekolwiek żądanie.
Przykład kodu — Produkcja
Dział zatytułowany „Przykład kodu — Produkcja”W środowisku produkcyjnym podłącz fabrykę lokalnego renderera, aby gdy Worker jest nieosiągalny, mostek przełączał się na mechanizm awaryjny zamiast powodować niepowodzenie żądania. Skonfiguruj przypięcia TLS wraz z przypięciem zapasowym. Metoda create() fabryki jest wywoływana tylko na ścieżce awaryjnej.
<?php
declare(strict_types=1);
use NextPDF\Artisan\ChromeHtmlRenderer;use NextPDF\Cloudflare\Contract\LocalRendererFactoryInterface;use NextPDF\Cloudflare\Contract\LocalRendererInterface;
final readonly class ArtisanLocalRendererFactory implements LocalRendererFactoryInterface{ public function __construct(private ChromeHtmlRenderer $chrome) {}
public function create(): LocalRendererInterface { return new readonly class($this->chrome) implements LocalRendererInterface { public function __construct(private ChromeHtmlRenderer $chrome) {}
/** @param array<string, mixed> $options */ public function render(string $html, array $options = []): string { $widthPt = (float) ($options['widthPt'] ?? 595.28); // A4 width $heightPt = (float) ($options['heightPt'] ?? 0.0); // 0 = auto-fit
return $this->chrome->render($html, $widthPt, $heightPt)->getPdfData(); } }; }}Podłącz fabrykę i przypięcia do renderera.
<?php
declare(strict_types=1);
use NextPDF\Cloudflare\CloudflareHtmlRenderer;use NextPDF\Cloudflare\CloudflareRendererConfig;
$config = CloudflareRendererConfig::fromArray([ 'worker_url' => getenv('CF_WORKER_URL') ?: '', 'api_token' => getenv('CF_PDF_TOKEN') ?: '', 'render_timeout' => 60, 'fallback_to_local' => true, 'pinned_public_keys' => ['sha256/YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg='], 'backup_public_keys' => ['sha256/Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys='],]);
$renderer = new CloudflareHtmlRenderer( config: $config, httpClient: $httpClient, requestFactory: $httpFactory, streamFactory: $httpFactory, logger: $logger, localRendererFactory: new ArtisanLocalRendererFactory($chrome), responseFactory: $httpFactory,);Gdy mechanizm awaryjny się uruchamia, wartość renderLocation wyniku to local, a heightPt wynosi 0.0. Mostek loguje przełączenie awaryjne na poziomie warning, a następnie info. Zawsze konfiguruj przypięcie zapasowe przed rotacją certyfikatu, aby zaplanowana rotacja nie odcięła mostka od punktu końcowego.
Przypadki brzegowe i pułapki
Dział zatytułowany „Przypadki brzegowe i pułapki”- Błąd Worker nie oznacza problemu z osiągalnością. Jeśli Worker zwraca błąd HTTP lub zniekształconą treść, mostek zgłasza
CloudflareRenderExceptioni nigdy nie ponawia żądania z użyciem mechanizmu awaryjnego. Tylko nieosiągalność brzegu sieci uruchamia mechanizm awaryjny. Obsługuj oba przypadki w oddzielnych catch. - Mechanizm awaryjny wymaga zarówno flagi, jak i fabryki. Przy
fallbackToLocal: true, lecz bez podłączonej fabryki, nieosiągalny Worker zgłaszaCloudflareNotAvailableExceptioni wskazuje brakującą fabrykę. Podłącz fabrykę. isAvailable()to wskazówka, a nie gwarancja. Wysyła uwierzytelnioneHEADi zwracatruedla statusu poniżej500; kolejnePOSTnadal może się nie powieść. Nie traktuj go jako kontraktu.- Przypinanie jest opcjonalne. Pusty zbiór przypięć wyłącza przypinanie. Używaj pustego zbioru tylko ze stabilnym, znanym łańcuchem certyfikatów i zachowaj przypięcie zapasowe, gdy już coś przypniesz.
fontFileswymaga zasobnika R2. ArgumentfontFilesma znaczenie tylko wtedy, gdy konfiguracja ustawiar2FontBucket; w przeciwnym razie nie ma efektu.- Mostek nie podpisuje. Zwraca bajty PDF. Renderuj na brzegu sieci, a następnie podpisuj we własnym procesie, aby klucz podpisujący nigdy nie przekraczał granicy brzegu sieci.
Wydajność
Dział zatytułowany „Wydajność”Renderowanie na brzegu sieci przenosi koszt przeglądarki poza hosty aplikacji. Nadal ponosisz koszt jednego wejścia-wyjścia HTTPS do Worker oraz czasu renderowania Worker, raportowanego w wyniku jako renderTimeMs. Mostek stosuje skonfigurowany limit czasu przez transport z przypinaniem. Ustaw go na podstawie zmierzonego opóźnienia Worker, z zapasem, i utrzymuj go poniżej dowolnego limitu czasu bramy nadrzędnej. Pakiet deklaruje wyłącznie limity, które sam egzekwuje. Nie formułuje żadnego twierdzenia o limitach platformy Cloudflare dotyczących CPU, pamięci ani rozmiaru treści żądania. W sprawie tych limitów zapoznaj się z dokumentacją Cloudflare oraz swoim Worker.
Uwagi dotyczące bezpieczeństwa
Dział zatytułowany „Uwagi dotyczące bezpieczeństwa”- Miejsce docelowe jest walidowane, zanim żądanie opuści PHP. Adresy URL spoza HTTPS są odrzucane. Host, który rozwiązuje się do prywatnej lub zarezerwowanej przestrzeni adresowej, jest odrzucany po sprawdzeniu wszystkich rekordów A i AAAA. Host jest ponownie rozwiązywany bezpośrednio przed połączeniem, aby bronić się przed DNS-rebinding.
- Transport z przypinaniem wiąże DNS i TLS. Gdy skonfigurowano fabrykę odpowiedzi i przypięcia, mostek wiąże połączenie ze zweryfikowanymi adresami IP, wymusza przypinanie SPKI, weryfikuje partnera i hosta oraz odmawia podążania za przekierowaniami do niezweryfikowanego hosta.
- Dane wejściowe są ograniczone. HTML przekraczający
maxHtmlSize(domyślnie 5 MB), zbyt duże identyfikatory URI danych w base64 oraz każdy znacznik<meta http-equiv="refresh">są odrzucane, zanim żądanie zostanie wysłane. - Sekrety są maskowane i niezmienne.
apiTokenoraz klucze R2 noszą atrybut#[SensitiveParameter], dzięki czemu ślady stosu je maskują, a obiekty konfiguracji sąfinal readonly. Wczytuj sekrety ze środowiska lub z menedżera sekretów; nigdy ich nie commituj. - Nigdy nie pisz pustego bloku
catch. Każdy przykład przechwytuje konkretny typ wyjątku i zapisuje go w dzienniku lub kończy działanie ze zdefiniowanym kodem.
Pełny model bezpieczeństwa znajduje się na stronie Cloudflare poświęconej bezpieczeństwu i eksploatacji w sekcji Zobacz także. Obejmuje on obronę przed SSRF i DNS-rebinding, procedury przypinania, obsługę sekretów oraz odpowiednie klauzule OWASP i RFC 7469.
Zgodność
Dział zatytułowany „Zgodność”Ten przewodnik nie formułuje żadnego własnego normatywnego twierdzenia o standardach. Na nadrzędnych stronach Cloudflare poświęconych bezpieczeństwu i eksploatacji oraz konfiguracji rozwiązywanie wszystkich rekordów DNS przez mostek i ponowna kontrola pod kątem TOCTOU odpowiadają wytycznym OWASP dotyczącym zapobiegania SSRF, a jego przypinanie klucza publicznego TLS i odzyskiwanie za pomocą przypięcia zapasowego odpowiadają RFC 7469. Ta strona książki kucharskiej powtarza sposób użycia i odsyła po te cytowania do tamtych stron. Mostek nie wykonuje podpisywania i nie formułuje żadnego twierdzenia o zgodności podpisu.
Zobacz także
Dział zatytułowany „Zobacz także”- Renderowanie HTML do PDF za pomocą renderera Artisan Chrome — renderer działający w procesie, używany tutaj jako lokalny mechanizm awaryjny.
- Szybki start Cloudflare — pierwsze renderowanie na brzegu sieci i model wyniku.
- Bezpieczeństwo i eksploatacja Cloudflare — SSRF, DNS-rebinding, przypinanie i rotacja sekretów.
- Użycie produkcyjne Cloudflare — podłączanie mechanizmu awaryjnego, telemetria, archiwizacja R2 i ochrona API.