تخطَّ إلى المحتوى

العرض على الحافة باستخدام 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.⁩

Terminal window
composer require nextpdf/cloudflare guzzlehttp/guzzle

من أجل التراجع المحلي، ثبّت عارضًا محليًا يستطيع الجسر استدعاءه.

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

تستخدم render() افتراضيًا عرض ⁨A4⁩ (595.28 نقطة) وارتفاعًا يُكتشف تلقائيًا (heightPt: 0). للاطلاع على المرجع الكامل للحقول وخريطة مفاتيح fromArray()، راجع صفحة إعداد ⁨Cloudflare⁩ ضمن انظر أيضًا.

أنشئ الإعداد، وابنِ العارض، واعرض، واكتب البايتات.

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

يأتي الرمز من البيئة ولا يُكتب مباشرةً في الشيفرة مطلقًا. استخدم بروتوكول ⁨HTTPS⁩ في workerUrl؛ إذ يرفض الجسر عنوان ⁨URL⁩ من نوع http:// قبل أن يرسل أي طلب.

في الإنتاج، اربط مصنع عارض محلي حتى يُستخدم التراجع عند تعذّر الوصول إلى ⁨Worker⁩ بدلًا من إفشال الطلب. اضبط تثبيتات ⁨TLS⁩ مع تثبيت احتياطي. لا تعمل create() الخاصة بالمصنع إلا على مسار التراجع.

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

اربط المصنع والتثبيتات بالعارض.

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

عند تشغيل التراجع، تكون 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. تعيد صفحة دليل الوصفات هذه ذكر الاستخدام وتحيل الاستشهادات إلى تلك الصفحات. لا يجري الجسر أي توقيع ولا يقدّم أي ادعاء بمطابقة التوقيع.