İçeriğe geç

Cloudflare ile uçta işleme ve yerel yedek

Cloudflare köprüsü, HTML’inizi bir Cloudflare Worker işleme uç noktasına gönderir ve PDF’yi döndürür. İşleme uçta çalıştığı için uzun ömürlü bir tarayıcı işlemi çalıştırmanız gerekmez. Yalnızca HTTPS kullanan bir yapılandırma oluşturur, bir PHP Standards Recommendation (PSR)-18 istemcisi ile PSR-17 fabrikalarını bağlar, render() çağrısı yapar ve erişilemeyen Worker’lar için yerel bir işleyici ekleyebilirsiniz. Bu kılavuz, işleme çağrısını, yedek yolunu ve köprünün herhangi bir istek PHP sürecinden çıkmadan önce uyguladığı Sunucu Tarafı İstek Sahteciliği (SSRF), Alan Adı Sistemi (DNS) yeniden bağlama ve Taşıma Katmanı Güvenliği (TLS) genel anahtar sabitleme denetimlerini gösterir.

Başlamadan önce:

  • NextPDF core ve nextpdf/cloudflare kurulu olmalıdır.
  • Bir Worker uç noktası, işleme sözleşmesini HTTPS üzerinden sunmalı ve bir taşıyıcı belirteci (bearer token) kabul etmelidir. Köprü, herhangi bir şey göndermeden önce HTTPS kullanmayan bir Worker URL’sini reddeder.
  • Bir PSR-18 istemcisi (örneğin Guzzle 7) ile PSR-17 istek ve akış fabrikaları mevcut olmalıdır. Sabitlenmiş cURL taşıması için ayrıca bir PSR-17 yanıt fabrikası ve ext-curl sağlayın.
  • Yerel yedek için nextpdf/artisan (veya başka bir yerel işleyici) mevcut olmalıdır.

Bu bir nasıl-yapılır kılavuzudur. İlk çalışır işlemeniz için Cloudflare hızlı başlangıç kılavuzuyla başlayın.

Köprüyü, bir PSR-18 istemcisini ve PSR-17 fabrikalarını kurun.

Terminal window
composer require nextpdf/cloudflare guzzlehttp/guzzle

Yerel yedek için köprünün çağırabileceği yerel bir işleyici kurun.

Terminal window
composer require nextpdf/artisan

Worker taşıyıcı belirtecini ve tüm R2 kimlik bilgilerini ortam değişkenlerinden veya bir gizli dizi yöneticisinden yükleyin. Bunları asla işleme (commit) eklemeyin.

CloudflareHtmlRenderer::render() HTML’i ve hedefi doğrular, Worker’a kimliği doğrulanmış bir POST gönderir ve yanıtı ayrıştırır. Worker, ham PDF baytlarını (Content-Type: application/pdf) veya base64 kodlu bir pdf alanı içeren bir JSON gövdesi döndürür. İşleyici, yanıtı bir final readonly CloudflareRenderResult nesnesine eşler; bu nesne baytları, istenen genişliği, yüksekliği, işleme konumunu (CF-Ray başlığından türetilir) ve işleme süresini taşır.

Köprü, başarısızlıkları iki net sınıfa ayırır:

  • CloudflareRenderException — Worker yanıt verdi ancak işleme başarısız oldu (bir HTTP hatası veya %PDF ile başlamayan bir gövde). Bu bir işleme başarısızlığıdır ve yedekle asla yeniden denenmez.
  • CloudflareNotAvailableException — uç noktasına erişilemedi ve kullanılabilir bir yedek mevcut değildi.

Yerel yedek, ikinci durumu kapsar. Worker’a erişilemediğinde ve fallbackToLocal değeri true olduğunda köprü, sağladığınız LocalRendererFactoryInterface arabirimini çağırır. Bunu tembel biçimde yapar: fabrikanın create() yöntemi yalnızca yedek yolunda çalışır. Yedekli bir işlemede sonucun renderLocation değeri, birebir local dizesidir.

Köprü, herhangi bir istek PHP sürecinden çıkmadan önce ağ sınırını korur. HTTPS olmayan bir Worker URL’sini reddeder. Yalnızca ilkini değil tüm A ve AAAA kayıtlarını denetleyerek, özel veya ayrılmış adres alanına çözümlenen bir Worker ana bilgisayarını reddeder. Ayrıca bağlanmadan hemen önce ana bilgisayarı yeniden çözümler; bu da DNS yeniden bağlamaya karşı time-of-check/time-of-use (TOCTOU) penceresini kapatır. Bir PSR-17 yanıt fabrikası ve çözümlenmiş bir IP kümesi ya da Subject Public Key Info (SPKI) sabitleri sağladığınızda köprü, sabitlenmiş bir cURL taşıması kullanır. Bu taşıma, bağlantıyı doğrulanmış IP’lere bağlar (CURLOPT_RESOLVE), TLS genel anahtar sabitlemeyi uygular (CURLOPT_PINNEDPUBLICKEY), eşi ve ana bilgisayarı doğrular ve yönlendirmeleri izlemez.

// 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() varsayılan olarak A4 genişliğini (595.28 punto) ve otomatik olarak algılanan yüksekliği (heightPt: 0) kullanır. Tam alan başvurusu ve fromArray() anahtar eşlemesi için Ayrıca bakınız bölümündeki Cloudflare yapılandırma sayfasına bakın.

Yapılandırmayı oluşturun, işleyiciyi kurun, işleyin ve baytları yazın.

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);
}

Belirteç ortamdan okunur ve asla koda sabit olarak gömülmez. workerUrl HTTPS kullanmalıdır; köprü, herhangi bir istek göndermeden önce bir http:// URL’sini reddeder.

Üretimde, erişilemeyen bir Worker nedeniyle isteğin başarısız olması yerine yedeğe geçmek için yerel bir işleyici fabrikası bağlayın. TLS sabitlerini bir yedek sabitle birlikte yapılandırın. Fabrikanın create() yöntemi yalnızca yedek yolunda çalışır.

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();
}
};
}
}

Fabrikayı ve sabitleri işleyiciye bağlayın.

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,
);

Yedek çalıştığında sonucun renderLocation değeri local, heightPt değeri ise 0.0 olur. Köprü, yedeği önce warning, ardından info düzeyinde günlüğe kaydeder. Sertifikayı döndürmeden önce her zaman bir yedek sabit yapılandırın; böylece planlı bir döndürme, köprünün uç noktasına erişimini kesmez.

  • Bir Worker hatası, erişilebilirlik başarısızlığı değildir. Bir HTTP hatası veya hatalı biçimlendirilmiş gövde döndüren bir Worker, CloudflareRenderException oluşturur ve yedekle asla yeniden denenmez. Yalnızca erişilemeyen bir uç nokta yedeğe geçer. İki catch kolunu ayrı tutun.
  • Yedek için hem bayrak hem de bir fabrika gerekir. fallbackToLocal: true ayarlı olduğunda ancak hiçbir fabrika bağlı olmadığında, erişilemeyen bir Worker CloudflareNotAvailableException oluşturur ve eksik fabrikanın adını belirtir. Fabrikayı bağlayın.
  • isAvailable() bir ipucudur, garanti değildir. Kimliği doğrulanmış bir HEAD gönderir ve true değerini 500 altındaki bir durum için döndürür; sonraki POST yine de başarısız olabilir. Bunu bir sözleşme olarak değerlendirmeyin.
  • Sabitleme isteğe bağlıdır. Boş bir sabit kümesi, sabitlemeyi devre dışı bırakır. Boş bir kümeyi yalnızca kararlı ve bilinen bir sertifika zinciriyle kullanın ve sabitleme yaptığınızda bir yedek sabit bulundurun.
  • fontFiles için bir R2 paketi (bucket) gerekir. fontFiles bağımsız değişkeni yalnızca yapılandırma r2FontBucket değerini ayarladığında önemlidir; aksi takdirde hiçbir etkisi yoktur.
  • Köprü imzalama yapmaz. PDF baytlarını döndürür. Uçta işleyin, ardından kendi işleminizde imzalayın; böylece imzalama anahtarı uç sınırını asla geçmez.

Uçta işleme, tarayıcı maliyetini kendi ana bilgisayarlarınızdan kaldırır. Yine de Worker’a yapılan bir HTTPS gidiş-dönüşünün ve Worker’ın işleme süresinin maliyetini üstlenirsiniz; sonuç bunu renderTimeMs olarak raporlar. Köprü, yapılandırılan zaman aşımını sabitlenmiş taşıma aracılığıyla uygular. Bunu, ölçülen Worker gecikmesine pay bırakacak şekilde ayarlayın ve herhangi bir üst akış ağ geçidi zaman aşımının altında tutun. Paket, yalnızca kendi uyguladığı sınırları belirtir. Cloudflare platformunun CPU, bellek veya istek gövdesi üst sınırları hakkında hiçbir iddiada bulunmaz. Bu sınırlar için Cloudflare belgelerine ve Worker’ınıza başvurun.

  • Hedef, istek PHP sürecinden çıkmadan önce doğrulanır. HTTPS olmayan URL’ler reddedilir. Özel veya ayrılmış adres alanına çözümlenen bir ana bilgisayar, tüm A ve AAAA kayıtları boyunca reddedilir. DNS yeniden bağlamaya karşı savunmak için ana bilgisayar, bağlanmadan hemen önce yeniden çözümlenir.
  • Sabitlenmiş taşıma, DNS ile TLS’yi bağlar. Bir yanıt fabrikası ve sabitler yapılandırıldığında köprü, bağlantıyı doğrulanmış IP’lere bağlar, SPKI sabitlemeyi uygular, eşi ve ana bilgisayarı doğrular ve doğrulanmamış bir ana bilgisayara yönlendirmeleri izlemeyi reddeder.
  • Girdi sınırlandırılır. maxHtmlSize değerini aşan HTML (varsayılan 5 MB), aşırı büyük bir base64 veri URI’si ve herhangi bir <meta http-equiv="refresh"> etiketi, istek gönderilmeden önce reddedilir.
  • Gizli diziler maskelenir ve değişmezdir. apiToken ve R2 anahtarları #[SensitiveParameter] taşır; bu nedenle yığın izleri (stack trace) bunları maskeler ve yapılandırma nesneleri final readonly niteliğindedir. Gizli dizileri ortamdan veya bir gizli dizi yöneticisinden yükleyin; bunları asla işleme (commit) eklemeyin.
  • Asla boş bir catch bloğu yazmayın. Her örnek, ilgili özel durum türünü yakalar ve günlüğe kaydeder veya tanımlı bir kodla çıkar.

Tam güvenlik modeli, Ayrıca bakınız bölümündeki Cloudflare güvenlik ve operasyonlar sayfasındadır. SSRF ve DNS yeniden bağlama savunmasını, sabitleme işlemlerini, gizli dizi yönetimini ve ilgili OWASP ile RFC 7469 maddelerini kapsar.

Bu kılavuz, tek başına hiçbir normatif standart iddiasında bulunmaz. Üst akıştaki Cloudflare güvenlik ve operasyonlar ile yapılandırma sayfalarında, köprünün tüm kayıtları kapsayan DNS çözümlemesi ve TOCTOU yeniden denetimi OWASP SSRF önleme kılavuzuna, TLS genel anahtar sabitlemesi ve yedek sabit kurtarması ise şu belgeye dayandırılır: RFC 7469. Bu yemek kitabı sayfası kullanımı yeniden açıklar ve bu atıfları söz konusu sayfalara bırakır. Köprü hiçbir imzalama yapmaz ve hiçbir imza uygunluğu iddiasında bulunmaz.