Przejdź do głównej zawartości

Renderowanie na brzegu sieci z Cloudflare i lokalnym mechanizmem awaryjnym

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/cloudflare są 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.

Zainstaluj mostek, klienta PSR-18 oraz fabryki PSR-17.

Okno terminala
composer require nextpdf/cloudflare guzzlehttp/guzzle

Na potrzeby lokalnego mechanizmu awaryjnego zainstaluj lokalny renderer, który mostek może wywołać.

Okno terminala
composer require nextpdf/artisan

Wczytaj token bearer dla Worker oraz wszelkie dane uwierzytelniające R2 ze zmiennych środowiskowych lub z menedżera sekretów. Nigdy ich nie commituj.

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.

// 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 = []): CloudflareRenderResult
CloudflareHtmlRenderer::isAvailable(): bool

render() 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.

Utwórz konfigurację, zbuduj renderer, wykonaj renderowanie i zapisz bajty.

edge-quickstart.php
<?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.

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.

ArtisanLocalRendererFactory.php
<?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.

build the production renderer
<?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.

  • Błąd Worker nie oznacza problemu z osiągalnością. Jeśli Worker zwraca błąd HTTP lub zniekształconą treść, mostek zgłasza CloudflareRenderException i 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łasza CloudflareNotAvailableException i wskazuje brakującą fabrykę. Podłącz fabrykę.
  • isAvailable() to wskazówka, a nie gwarancja. Wysyła uwierzytelnione HEAD i zwraca true dla statusu poniżej 500; kolejne POST nadal 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.
  • fontFiles wymaga zasobnika R2. Argument fontFiles ma znaczenie tylko wtedy, gdy konfiguracja ustawia r2FontBucket; 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.

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.

  • 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. apiToken oraz 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.

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.