العرض على الحافة باستخدام Cloudflare مع تراجع محلي
لمحة سريعة
قسم بعنوان «لمحة سريعة»يرسل جسر Cloudflare ملف HTML لديك إلى نقطة نهاية عرض على Cloudflare Worker ويعيد ملف PDF. يجري العرض على الحافة، لذلك لن تحتاج إلى تشغيل عملية متصفح طويلة الأمد. أنشئ إعدادًا يقتصر على HTTPS، واربط عميل PSR-18 ومصانع PSR-17، واستدعِ render()، ويمكنك إضافة عارض محلي عند تعذّر الوصول إلى Workers. يعرض هذا الدليل استدعاء العرض، ومسار التراجع، وضوابط تزوير الطلب من جانب الخادم (SSRF)، وإعادة ربط نظام أسماء النطاقات (DNS)، وتثبيت المفتاح العام لأمن طبقة النقل (TLS) التي يفرضها الجسر قبل خروج أي طلب من العملية.
المتطلبات الأساسية قبل البدء:
- جوهر NextPDF و
nextpdf/cloudflareمثبَّتان. - تقدّم نقطة نهاية Worker عقد العرض عبر HTTPS وتقبل رمز حامل. يرفض الجسر عنوان URL الخاص بـ Worker إذا لم يستخدم HTTPS، وذلك قبل أن يرسل أي شيء.
- يتوفر عميل PSR-18 (على سبيل المثال Guzzle 7) ومصانع الطلب والدفق المتوافقة مع PSR-17. لاستخدام نقل cURL المثبَّت، وفّر أيضًا مصنع استجابة متوافقًا مع PSR-17 و
ext-curl. - من أجل التراجع المحلي، يتوفر
nextpdf/artisan(أو عارض محلي آخر).
هذا دليل إرشادي. لأول عرض قابل للتشغيل لديك، ابدأ بدليل البدء السريع لـ Cloudflare.
التثبيت
قسم بعنوان «التثبيت»ثبّت الجسر، وعميل PSR-18، ومصانع PSR-17.
composer require nextpdf/cloudflare guzzlehttp/guzzleمن أجل التراجع المحلي، ثبّت عارضًا محليًا يستطيع الجسر استدعاءه.
composer require nextpdf/artisanحمّل رمز حامل Worker وأي بيانات اعتماد R2 من متغيرات البيئة أو من مدير أسرار. لا تُودِعها مطلقًا في نظام التحكم بالإصدارات.
نظرة عامة مفاهيمية
قسم بعنوان «نظرة عامة مفاهيمية»تتحقق CloudflareHtmlRenderer::render() من صحة HTML والوجهة، وترسل طلب POST مصادَقًا عليه إلى Worker، ثم تحلّل الاستجابة. يعيد Worker بايتات PDF خام (Content-Type: application/pdf) أو نص JSON يحتوي على حقل pdf بترميز base64. يحوّل العارض الاستجابة إلى final readonly CloudflareRenderResult يحمل البايتات، والعرض المطلوب، والارتفاع، وموقع العرض (المشتق من ترويسة CF-Ray)، ووقت العرض.
يفصل الجسر حالات الفشل إلى فئتين واضحتين:
CloudflareRenderException— استجاب Worker لكن فشل العرض (خطأ HTTP أو نص لا يبدأ بـ%PDF). هذا فشل في العرض ولا تُعاد محاولته مطلقًا مع تراجع.CloudflareNotAvailableException— تعذّر الوصول إلى الحافة ولم يتوفر تراجع قابل للاستخدام.
يغطي التراجع المحلي الحالة الثانية. عندما يتعذّر الوصول إلى Worker وتكون fallbackToLocal بقيمة true، يستدعي الجسر LocalRendererFactoryInterface الذي توفّره. ويتم ذلك عند الحاجة فقط: لا تعمل create() الخاصة بالمصنع إلا على مسار التراجع. في عرض التراجع، تكون renderLocation الخاصة بالنتيجة هي السلسلة الحرفية local.
يحمي الجسر حدود الشبكة قبل خروج أي طلب من PHP. يرفض عنوان URL الخاص بـ Worker إذا لم يستخدم HTTPS. ويرفض مضيف Worker الذي يُحَل إلى مساحة عناوين خاصة أو محجوزة، مع التحقق من جميع سجلات A وAAAA بدلًا من الاكتفاء بالسجل الأول. كما يعيد حل المضيف مباشرة قبل الاتصال، ما يغلق نافذة time-of-check/time-of-use (TOCTOU) في مواجهة إعادة ربط DNS. عندما توفّر مصنع استجابة متوافقًا مع PSR-17 وإما مجموعة عناوين IP محلولة أو تثبيتات معلومات المفتاح العام للموضوع (SPKI)، يستخدم الجسر نقل cURL مثبَّتًا. يربط هذا النقل الاتصال بعناوين IP المدقَّقة (CURLOPT_RESOLVE)، ويفرض تثبيت المفتاح العام لـ TLS (CURLOPT_PINNEDPUBLICKEY)، ويتحقق من النظير والمضيف، ولا يتبع عمليات إعادة التوجيه.
واجهة برمجة التطبيقات
قسم بعنوان «واجهة برمجة التطبيقات»// 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(): boolتستخدم render() افتراضيًا عرض A4 (595.28 نقطة) وارتفاعًا يُكتشف تلقائيًا (heightPt: 0). للاطلاع على المرجع الكامل للحقول وخريطة مفاتيح fromArray()، راجع صفحة إعداد Cloudflare ضمن انظر أيضًا.
مثال برمجي — البدء السريع
قسم بعنوان «مثال برمجي — البدء السريع»أنشئ الإعداد، وابنِ العارض، واعرض، واكتب البايتات.
<?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);}يأتي الرمز من البيئة ولا يُكتب مباشرةً في الشيفرة مطلقًا. استخدم بروتوكول HTTPS في workerUrl؛ إذ يرفض الجسر عنوان URL من نوع http:// قبل أن يرسل أي طلب.
مثال برمجي — الإنتاج
قسم بعنوان «مثال برمجي — الإنتاج»في الإنتاج، اربط مصنع عارض محلي حتى يُستخدم التراجع عند تعذّر الوصول إلى Worker بدلًا من إفشال الطلب. اضبط تثبيتات TLS مع تثبيت احتياطي. لا تعمل create() الخاصة بالمصنع إلا على مسار التراجع.
<?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(); } }; }}اربط المصنع والتثبيتات بالعارض.
<?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,);عند تشغيل التراجع، تكون renderLocation الخاصة بالنتيجة هي local وتكون heightPt بقيمة 0.0. يسجّل الجسر التراجع عند مستوى warning، ثم info. اضبط دائمًا تثبيتًا احتياطيًا قبل تدوير الشهادة، حتى لا يمنع التدوير المخطَّط الجسر من الوصول إلى نقطة النهاية.
الحالات الحدّية والمزالق
قسم بعنوان «الحالات الحدّية والمزالق»- خطأ Worker ليس فشلًا في إمكانية الوصول. إن Worker الذي يعيد خطأ HTTP أو نصًا مشوَّهًا يثير
CloudflareRenderExceptionولا تُعاد محاولته مع التراجع مطلقًا. لا يحدث التراجع إلا عند تعذّر الوصول إلى الحافة. أبقِ فرعَي catch منفصلين. - يحتاج التراجع إلى الراية والمصنع معًا. عند ضبط
fallbackToLocal: trueمن دون ربط مصنع، يثير Worker غير القابل للوصولCloudflareNotAvailableExceptionويذكر المصنع المفقود. اربط المصنع. isAvailable()تلميح، وليست ضمانًا. ترسلHEADمصادَقًا عليه وتعيدtrueلأي حالة أقل من500؛ ومع ذلك قد يفشلPOSTالذي يليه. لا تتعامل معها على أنها عقد.- التثبيت اختياري ويحتاج إلى ضبط صريح. تعطّل مجموعة التثبيتات الفارغة التثبيت. لا تستخدم مجموعة فارغة إلا مع سلسلة شهادات مستقرة ومعروفة، واحتفظ بتثبيت احتياطي بمجرد تفعيل التثبيت.
- تحتاج
fontFilesإلى دلو R2. لا يكون لوسيطةfontFilesأثر إلا عندما يضبط الإعدادr2FontBucket؛ وإلا فلا أثر لها. - الجسر لا يوقّع. يعيد بايتات PDF. اعرض على الحافة، ثم وقّع في عمليتك الخاصة، حتى لا يعبر مفتاح التوقيع حدود الحافة مطلقًا.
الأداء
قسم بعنوان «الأداء»ينقل العرض على الحافة تكلفة المتصفح بعيدًا عن مضيفاتك. لا تزال تتحمل تكلفة جولة HTTPS واحدة ذهابًا وإيابًا إلى Worker إضافةً إلى وقت عرض Worker، الذي تبلّغ عنه النتيجة باسم renderTimeMs. يطبّق الجسر المهلة المُعَدّة عبر النقل المثبَّت. اضبطها بناءً على زمن استجابة Worker المقيس مع هامش مناسب، وأبقِها دون أي مهلة بوابة في المسار الأعلى. لا تذكر الحزمة سوى الحدود التي تفرضها بنفسها. ولا تدّعي أي شيء عن سقوف وحدة المعالجة المركزية أو الذاكرة أو نص الطلب في منصة Cloudflare. للاطلاع على تلك الحدود، راجع وثائق Cloudflare وWorker الخاص بك.
ملاحظات أمنية
قسم بعنوان «ملاحظات أمنية»- يُتحقق من الوجهة قبل خروج الطلب من PHP. تُرفض عناوين URL التي لا تستخدم HTTPS. ويُرفض المضيف الذي يُحَل إلى مساحة عناوين خاصة أو محجوزة عبر جميع سجلات A وAAAA. يُعاد حل المضيف مباشرة قبل الاتصال، للدفاع ضد إعادة ربط DNS.
- يربط النقل المثبَّت DNS وTLS. عند ضبط مصنع استجابة وتثبيتات، يربط الجسر الاتصال بعناوين IP المدقَّقة، ويفرض تثبيت SPKI، ويتحقق من النظير والمضيف، ويرفض اتباع عمليات إعادة التوجيه إلى مضيف غير مدقَّق.
- المُدخل محدود. يُرفض HTML الذي يتجاوز
maxHtmlSize(افتراضيًا 5 MB)، وعنوان URI لبيانات base64 المفرط الحجم، وأي وسم<meta http-equiv="refresh">قبل أن يُرسَل الطلب. - الأسرار محجوبة وغير قابلة للتغيير. يحمل
apiTokenومفاتيح R2 السمة#[SensitiveParameter]، لذا تحجبها آثار المكدّس، وتكون كائنات الإعدادfinal readonly. حمّل الأسرار من البيئة أو من مدير أسرار، ولا تُودِعها في نظام التحكم بالإصدارات مطلقًا. - لا تكتب كتلة
catchفارغة مطلقًا. يلتقط كل مثال نوع الاستثناء المحدد ويسجّل الخطأ أو يخرج برمز معرَّف.
يوجد نموذج الأمان الكامل في صفحة Cloudflare للأمان والعمليات ضمن انظر أيضًا. وتغطي الصفحة الدفاع ضد SSRF وإعادة ربط DNS، وعمليات التثبيت، ومعالجة الأسرار، وبنود OWASP وRFC 7469 ذات الصلة.
المطابقة
قسم بعنوان «المطابقة»لا يقدّم هذا الدليل أي ادعاء معياري خاص به بشأن المعايير. في صفحتي Cloudflare للأمان والعمليات والإعداد أعلى المسار، تتطابق دقة حل DNS لجميع السجلات وإعادة فحص TOCTOU في الجسر مع إرشادات منع SSRF من OWASP، ويتطابق تثبيت المفتاح العام لـ TLS والاستعادة باستخدام التثبيت الاحتياطي مع RFC 7469. تعيد صفحة دليل الوصفات هذه ذكر الاستخدام وتحيل الاستشهادات إلى تلك الصفحات. لا يجري الجسر أي توقيع ولا يقدّم أي ادعاء بمطابقة التوقيع.
انظر أيضًا
قسم بعنوان «انظر أيضًا»- عرض HTML إلى PDF باستخدام عارض Artisan Chrome — العارض داخل العملية المستخدَم هنا بوصفه التراجع المحلي.
- دليل البدء السريع لـ Cloudflare — أول عرض على الحافة ونموذج النتيجة.
- أمان Cloudflare وعملياتها — SSRF، وإعادة ربط DNS، والتثبيت، وتدوير الأسرار.
- استخدام Cloudflare في الإنتاج — ربط التراجع، والقياس عن بُعد، والأرشفة في R2، وحماية واجهة برمجة التطبيقات.