เรนเดอร์ที่เอดจ์ด้วย Cloudflare พร้อม fallback ภายในเครื่อง
ภาพรวมโดยสรุป
หัวข้อที่มีชื่อว่า “ภาพรวมโดยสรุป”Cloudflare bridge ส่ง HTML ของคุณไปยัง render endpoint ของ Cloudflare Worker แล้วคืนผลลัพธ์เป็น PDF การเรนเดอร์ทำงานที่เอดจ์ คุณจึงไม่ต้องดูแลโปรเซสเบราว์เซอร์ที่ทำงานต่อเนื่องยาวนาน คุณสร้าง config ที่ใช้เฉพาะ HTTPS เชื่อมต่อไคลเอนต์ PHP Standards Recommendation (PSR)-18 และ factory แบบ PSR-17 เรียกใช้ render() และสามารถเพิ่ม renderer ภายในเครื่องสำหรับกรณีที่เข้าถึง Worker ไม่ได้ คู่มือนี้แสดงการเรียก render เส้นทาง fallback และมาตรการควบคุม Server-Side Request Forgery (SSRF) การโจมตีแบบ Domain Name System (DNS)-rebinding และ Transport Layer Security (TLS) public-key-pinning ที่ bridge บังคับใช้ก่อนที่คำขอใดๆจะออกจากโปรเซส
ข้อกำหนดเบื้องต้นโดยสรุปก่อนเริ่ม:
- ติดตั้ง NextPDF core และ
nextpdf/cloudflareแล้ว - endpoint ของ Worker ให้บริการ render contract ผ่าน HTTPS และรับ bearer token โดย bridge จะปฏิเสธ URL ของ Worker ที่ไม่ใช่ HTTPS ก่อนส่งสิ่งใดออกไป
- มีไคลเอนต์ PSR-18 (เช่น Guzzle 7) และ request กับ stream factory แบบ PSR-17 พร้อมใช้งาน หากต้องใช้ pinned cURL transport ต้องจัดเตรียม response factory แบบ PSR-17 และ
ext-curlด้วย - สำหรับ fallback ภายในเครื่อง ต้องมี
nextpdf/artisan(หรือ renderer ภายในเครื่องอื่น) พร้อมใช้งาน
คู่มือนี้เป็น how-to สำหรับการเรนเดอร์ครั้งแรกที่รันได้จริง ให้เริ่มจาก Cloudflare quickstart
การติดตั้ง
หัวข้อที่มีชื่อว่า “การติดตั้ง”ติดตั้ง bridge พร้อมไคลเอนต์ PSR-18 และ factory แบบ PSR-17
composer require nextpdf/cloudflare guzzlehttp/guzzleสำหรับ fallback ภายในเครื่อง ให้ติดตั้ง renderer ภายในเครื่องที่ bridge สามารถเรียกใช้ได้
composer require nextpdf/artisanโหลด bearer token ของ Worker และข้อมูลรับรอง R2 ใดๆจาก environment variable หรือ secrets manager ห้าม commit ข้อมูลเหล่านี้โดยเด็ดขาด
ภาพรวมเชิงแนวคิด
หัวข้อที่มีชื่อว่า “ภาพรวมเชิงแนวคิด”CloudflareHtmlRenderer::render() ตรวจสอบความถูกต้องของ HTML และปลายทาง ส่ง POST ที่ผ่านการยืนยันตัวตนไปยัง Worker แล้วแยกวิเคราะห์การตอบกลับ Worker จะคืนค่าเป็น byte ดิบของ PDF (Content-Type: application/pdf) หรือ body แบบ JSON ที่มีฟิลด์ pdf ซึ่งเข้ารหัสแบบ base64 renderer แมปการตอบกลับไปยัง final readonly CloudflareRenderResult ซึ่งบรรจุ byte ของ PDF ความกว้างที่ร้องขอ ความสูง ตำแหน่งการเรนเดอร์ (ได้มาจากเฮดเดอร์ CF-Ray) และเวลาที่ใช้เรนเดอร์
bridge แยกความล้มเหลวออกเป็นสองคลาสอย่างชัดเจน:
CloudflareRenderException— Worker ตอบกลับแล้วแต่การเรนเดอร์ล้มเหลว (ไม่ว่าจะเป็นข้อผิดพลาด HTTP หรือ body ที่ไม่ได้ขึ้นต้นด้วย%PDF) กรณีนี้คือความล้มเหลวในการเรนเดอร์ และจะ ไม่มี การลองใหม่ด้วย fallbackCloudflareNotAvailableException— ไม่สามารถเข้าถึงเอดจ์ได้ และไม่มี fallback ที่ใช้งานได้
fallback ภายในเครื่องครอบคลุมกรณีที่สอง เมื่อเข้าถึง Worker ไม่ได้และ fallbackToLocal เป็น true bridge จะเรียกใช้ LocalRendererFactoryInterface ที่คุณจัดเตรียมไว้ โดยทำงานแบบ lazy: create() ของ factory จะทำงานเฉพาะบนเส้นทาง fallback เท่านั้น เมื่อเรนเดอร์ผ่าน fallback ค่า renderLocation ในผลลัพธ์จะเป็นสตริงตรงตัวว่า local
bridge ปกป้องขอบเขตเครือข่ายก่อนที่คำขอใดๆจะออกจาก PHP โดยปฏิเสธ URL ของ Worker ที่ไม่ใช่ HTTPS และปฏิเสธโฮสต์ของ Worker ที่ resolve ไปยังพื้นที่แอดเดรสแบบ private หรือ reserved โดยตรวจสอบเรกคอร์ด A และ AAAA ทั้งหมด ไม่ใช่เพียงเรกคอร์ดแรกเท่านั้น นอกจากนี้ยัง resolve โฮสต์ใหม่ทันทีก่อนเชื่อมต่อ เพื่อปิดช่องโหว่ time-of-check/time-of-use (TOCTOU) ที่ใช้โจมตีแบบ DNS rebinding เมื่อคุณจัดเตรียม response factory แบบ PSR-17 พร้อมชุด IP ที่ resolve แล้ว หรือ pin แบบ Subject Public Key Info (SPKI) อย่างใดอย่างหนึ่ง bridge จะใช้ pinned cURL transport โดย transport ดังกล่าวผูกการเชื่อมต่อไว้กับ IP ที่ผ่านการตรวจสอบแล้ว (CURLOPT_RESOLVE) บังคับใช้ TLS public-key pinning (CURLOPT_PINNEDPUBLICKEY) ตรวจสอบ peer และโฮสต์ และไม่ติดตาม redirect
ส่วนต่อประสาน API
หัวข้อที่มีชื่อว่า “ส่วนต่อประสาน 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() มีค่าเริ่มต้นเป็นความกว้างขนาด A4 (595.28 point) และความสูงที่ตรวจหาอัตโนมัติ (heightPt: 0) สำหรับรายการฟิลด์ทั้งหมดและ key map ของ fromArray() โปรดดูหน้าการกำหนดค่า Cloudflare ในส่วนดูเพิ่มเติม
ตัวอย่างโค้ด — เริ่มต้นใช้งานอย่างรวดเร็ว
หัวข้อที่มีชื่อว่า “ตัวอย่างโค้ด — เริ่มต้นใช้งานอย่างรวดเร็ว”สร้าง config และ renderer จากนั้นเรนเดอร์และเขียน byte
<?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 มาจาก environment และห้าม hard-code โดยเด็ดขาด workerUrl ต้องใช้ HTTPS โดย bridge จะปฏิเสธ URL แบบ http:// ก่อนที่จะส่งคำขอใดๆ
ตัวอย่างโค้ด — โปรดักชัน
หัวข้อที่มีชื่อว่า “ตัวอย่างโค้ด — โปรดักชัน”ในโปรดักชัน ให้เชื่อมต่อ local renderer factory เพื่อให้กรณีที่เข้าถึง Worker ไม่ได้เปลี่ยนไปใช้ fallback แทนการทำให้คำขอล้มเหลว กำหนดค่า TLS pin พร้อม backup pin create() ของ factory จะทำงานเฉพาะบนเส้นทาง fallback เท่านั้น
<?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(); } }; }}เชื่อมต่อ factory และ pin เข้ากับ 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,);เมื่อ fallback ทำงาน ค่า renderLocation ในผลลัพธ์จะเป็น local และ heightPt จะเป็น 0.0 โดย bridge จะบันทึก log ของ fallback ที่ระดับ warning แล้วตามด้วย info ให้กำหนดค่า backup pin ก่อนการหมุนเวียนใบรับรองเสมอ เพื่อไม่ให้การหมุนเวียนที่วางแผนไว้ทำให้ bridge ถูกล็อกออกจาก endpoint
กรณีขอบและข้อควรระวัง
หัวข้อที่มีชื่อว่า “กรณีขอบและข้อควรระวัง”- ข้อผิดพลาดของ Worker ไม่ใช่ความล้มเหลวด้านการเข้าถึง Worker ที่คืนค่าข้อผิดพลาด HTTP หรือ body ที่ผิดรูปแบบจะทำให้เกิด
CloudflareRenderExceptionและจะไม่มีการลองใหม่ด้วย fallback เฉพาะเอดจ์ที่เข้าถึงไม่ได้เท่านั้นที่จะใช้ fallback ให้แยก catch ทั้งสองส่วนออกจากกันอย่างชัดเจน - fallback ต้องมีทั้ง flag และ factory หากตั้ง
fallbackToLocal: trueแต่ไม่ได้เชื่อมต่อ factory ไว้ Worker ที่เข้าถึงไม่ได้จะทำให้เกิดCloudflareNotAvailableExceptionและระบุชื่อ factory ที่ขาดหายไป จึงต้องเชื่อมต่อ factory isAvailable()เป็นเพียงคำใบ้ ไม่ใช่การรับประกัน เมธอดนี้ส่งHEADที่ผ่านการยืนยันตัวตน และคืนค่าtrueสำหรับสถานะที่ต่ำกว่า500แต่POSTที่ตามมายังคงล้มเหลวได้ อย่าถือว่าค่านี้เป็น contract- การ pinning เป็นแบบเลือกเปิดใช้ ชุด pin ที่ว่างเปล่าจะปิดใช้งานการ pinning ใช้ชุดที่ว่างเปล่าเฉพาะกับ certificate chain ที่เสถียรและเป็นที่รู้จักเท่านั้น และเมื่อ pin แล้วให้เก็บ backup pin ไว้เสมอ
fontFilesต้องมี R2 bucket อาร์กิวเมนต์fontFilesจะมีผลเฉพาะเมื่อ config ตั้งค่าr2FontBucketเท่านั้น มิฉะนั้นจะไม่มีผลใดๆ- bridge ไม่ทำการเซ็น bridge จะคืนค่าเป็น byte ของ PDF ให้เรนเดอร์ที่เอดจ์ แล้วจึงเซ็นในโปรเซสของคุณเอง เพื่อให้คีย์สำหรับเซ็นไม่ข้ามขอบเขตของเอดจ์โดยเด็ดขาด
ประสิทธิภาพ
หัวข้อที่มีชื่อว่า “ประสิทธิภาพ”การเรนเดอร์ที่เอดจ์ย้ายต้นทุนของเบราว์เซอร์ออกจากโฮสต์ของคุณ คุณยังคงมีต้นทุนจาก HTTPS round trip หนึ่งครั้งไปยัง Worker บวกกับเวลาที่ Worker ใช้เรนเดอร์ ซึ่งผลลัพธ์รายงานเป็น renderTimeMs bridge ใช้ timeout ที่กำหนดค่าไว้ผ่าน pinned transport ให้ตั้งค่าจาก latency ของ Worker ที่วัดได้พร้อมเผื่อ headroom และให้ค่านี้ต่ำกว่า timeout ของ gateway ต้นทางใดๆ แพ็กเกจระบุเฉพาะขีดจำกัดที่ตัวมันเองบังคับใช้เท่านั้น แพ็กเกจไม่ได้กล่าวอ้างเกี่ยวกับเพดาน CPU หน่วยความจำ หรือ request-body ของแพลตฟอร์ม Cloudflare สำหรับขีดจำกัดเหล่านั้น โปรดดูเอกสารของ Cloudflare และ Worker ของคุณ
บันทึกด้านความปลอดภัย
หัวข้อที่มีชื่อว่า “บันทึกด้านความปลอดภัย”- ปลายทางจะได้รับการตรวจสอบความถูกต้องก่อนที่คำขอจะออกจาก PHP URL ที่ไม่ใช่ HTTPS จะถูกปฏิเสธ โฮสต์ที่ resolve ไปยังพื้นที่แอดเดรสแบบ private หรือ reserved จะถูกปฏิเสธในทุกเรกคอร์ด A และ AAAA และโฮสต์จะถูก resolve ใหม่ทันทีก่อนเชื่อมต่อ เพื่อป้องกันการโจมตีแบบ DNS rebinding
- pinned transport ผูก DNS และ TLS เมื่อกำหนดค่า response factory และ pin ไว้ bridge จะผูกการเชื่อมต่อไว้กับ IP ที่ผ่านการตรวจสอบแล้ว บังคับใช้การ pinning แบบ SPKI ตรวจสอบ peer และโฮสต์ และปฏิเสธการติดตาม redirect ไปยังโฮสต์ที่ยังไม่ผ่านการตรวจสอบ
- อินพุตถูกจำกัดขอบเขต HTML ที่เกิน
maxHtmlSize(ค่าเริ่มต้น 5 MB) data URI แบบ base64 ที่มีขนาดใหญ่เกินไป และแท็ก<meta http-equiv="refresh">ใดๆจะถูกปฏิเสธก่อนที่จะส่งคำขอ - ความลับถูกปกปิดและไม่สามารถเปลี่ยนแปลงได้
apiTokenและคีย์ R2 มี#[SensitiveParameter]กำกับ ดังนั้น stack trace จึงปกปิดค่าเหล่านี้ และอ็อบเจกต์ config เป็นfinal readonlyโหลดความลับจาก environment หรือ secrets manager และห้าม commit ความลับเหล่านี้โดยเด็ดขาด - อย่าเขียนบล็อก
catchที่ว่างเปล่าโดยเด็ดขาด แต่ละตัวอย่างจับ exception ชนิดที่เฉพาะเจาะจง แล้วบันทึก log หรือออกด้วยโค้ดที่กำหนดไว้
โมเดลความปลอดภัยฉบับเต็มอยู่ในหน้า security-and-operations ของ Cloudflare ในส่วนดูเพิ่มเติม หน้าดังกล่าวครอบคลุมการป้องกัน SSRF และ DNS-rebinding การปฏิบัติของ pinning การจัดการความลับ และข้อกำหนดของ OWASP และ RFC 7469 ที่เกี่ยวข้อง
ความสอดคล้อง
หัวข้อที่มีชื่อว่า “ความสอดคล้อง”คู่มือนี้ไม่ได้กล่าวอ้างมาตรฐานเชิงบรรทัดฐานใดๆด้วยตัวเอง ในหน้า security-and-operations และการกำหนดค่าต้นทางของ Cloudflare การ resolve DNS แบบครบทุกเรกคอร์ดและการตรวจสอบ TOCTOU ซ้ำของ bridge สอดคล้องกับแนวทางการป้องกัน SSRF ของ OWASP และการ pinning แบบ TLS-public-key พร้อมการกู้คืนด้วย backup-pin สอดคล้องกับ RFC 7469 หน้า cookbook นี้กล่าวซ้ำถึงวิธีใช้งานและอ้างอิงการกล่าวอ้างเหล่านั้นไปยังหน้าดังกล่าว bridge ไม่ดำเนินการเซ็นใดๆ และไม่กล่าวอ้างความสอดคล้องของลายเซ็น
ดูเพิ่มเติม
หัวข้อที่มีชื่อว่า “ดูเพิ่มเติม”- เรนเดอร์ HTML เป็น PDF ด้วย Artisan Chrome renderer — renderer แบบ in-process ที่ใช้เป็น fallback ภายในเครื่องที่นี่
- Cloudflare quickstart — การเรนเดอร์ที่เอดจ์ครั้งแรกและโมเดลผลลัพธ์
- Cloudflare security and operations — SSRF DNS-rebinding การ pinning และการหมุนเวียนความลับ
- Cloudflare production usage — การเชื่อมต่อ fallback การวัดและส่งข้อมูล telemetry การจัดเก็บถาวรบน R2 และการป้องกัน API