تأیید یک تحویل
`OpenEmail::verifyWebhookSignature`: با زمان ثابت، با پنجرهٔ بازپخش، و با بازگرداندن رویداد تجزیهشده.
در یک هندلر درخواست
URL یک وبهوک عمومی است. هر چیزی در اینترنت میتواند JSONی با شکل درست به آن POST کند، پس هندلری که type رویداد را بدون بررسی امضا بخواند، یک API نوشتنِ باز است.
use OpenEmail\Exception\WebhookSignatureException;use OpenEmail\OpenEmail; $payload = (string) file_get_contents('php://input'); try { $event = OpenEmail::verifyWebhookSignature( $payload, $_SERVER, (string) getenv('OPENEMAIL_WEBHOOK_SECRET'), toleranceSeconds: 300, );} catch (WebhookSignatureException) { http_response_code(400); return;} error_log($event['type'] . ' ' . $event['id']);http_response_code(204);بدنهٔ «خام» را بهصورت یک رشته بدهید. دیکد کردن آن و کدگذاری دوباره، ترتیب کلیدها و فاصلهها را تغییر میدهد و امضا مطابقت نمیکند، و به همین دلیل نوع payload برابر string تعریف شده است: یک آرایه، مانند نتیجهٔ json_decode() یا ورودیِ تجزیهشده توسط یک فریمورک، بهجای بررسی شدن یک TypeError است. سرآیندها میتوانند آرایهای با نام سرآیند با هر بزرگی و کوچکی حروف باشند، همانطور که getallheaders() برمیگرداند، یا $_SERVER، که سرآیند در آن به شکل HTTP_X_OPENEMAIL_SIGNATURE میرسد، یا هر iterable مانند HeaderBag در Symfony یا Laravel، یا یک درخواست PSR-7. مقداری که آرایه باشد از نخستین عنصرش خوانده میشود.
دو چیزی که این متد رسیدگی میکند و بررسیِ دستساز معمولاً نه: MAC را با hash_equals() در زمان ثابت مقایسه میکند، تا پیشوند درست با زمانسنجی بازیابی نشود، و تحویلی را که در هر دو جهت بیش از toleranceSeconds: فاصله داشته باشد رد میکند، که پنج دقیقه است مگر چیز دیگری بگویید، تا درخواستی که شنود شده تا ابد قابل بازپخش نباشد. toleranceSeconds: 0 بررسی بازپخش را خاموش میکند. هر دو باگ بیصدا هستند. هندلری که هرکدام را داشته باشد از همهٔ آزمونهایی که به فکرتان برسد سربلند بیرون میآید.
در هر شکستی OpenEmail\Exception\WebhookSignatureException را پرتاب میکند: نبودن سرآیند X-OpenEmail-Signature، سرآیندی که به شکل t=<seconds>,v1=<hex> نباشد، مهر زمانیای بیرون از پنجره، امضایی که مطابقت نکند، یا بدنهٔ امضاشدهای که شیء JSON نباشد. در صورت موفقیت، بدنه را دیکدشده به یک آرایه با کلیدهای camelCase برمیگرداند، با id، type، createdAt و data، پس json_decode() دومی نیست که اشتباه انجام شود.
یک secret خالی بهجای آن OpenEmail\Exception\InvalidArgumentException را پرتاب میکند، چون این اشتباهی در پیکربندی شماست نه تحویلی بد، و (string) پیش از getenv() متغیری را که تنظیم نشده دقیقاً به همین تبدیل میکند. فقط WebhookSignatureException را بگیرید و 400 پاسخ دهید. در این صورت نبودن secret اسکریپت را با یک استثنای گرفتهنشده تمام میکند، که PHP در محیط عملیاتی با 500 به آن پاسخ میدهد، و تحویل پس از رفع مشکل دوباره امتحان میشود، بهجای آنکه همهٔ رویدادها بهعنوان جعلی رد شوند.
این بررسی روی hash_hmac() و hash_equals() اجرا میشود که در هر بیلد PHP هست، پس به هیچ افزونهای نیاز ندارد. نمونه بهجای فراخوانی exit از return استفاده میکند، پس همین خطوط درون یک تابع یا یک front controller هم کار میکنند.
در یک برنامهٔ PSR-7
با یک درخواست PSR-7، مانند آنچه Slim و Mezzio به شما میدهند، خودِ درخواست را بهعنوان سرآیندها و بدنهاش را بهصورت یک رشته بدهید.
use OpenEmail\Exception\WebhookSignatureException;use OpenEmail\OpenEmail;use Psr\Http\Message\ResponseFactoryInterface;use Psr\Http\Message\ResponseInterface;use Psr\Http\Message\ServerRequestInterface; function receiveOpenEmail(ServerRequestInterface $request, ResponseFactoryInterface $responses): ResponseInterface{ try { $event = OpenEmail::verifyWebhookSignature( (string) $request->getBody(), $request, (string) getenv('OPENEMAIL_WEBHOOK_SECRET'), ); } catch (WebhookSignatureException) { return $responses->createResponse(400); } error_log($event['type'] . ' ' . $event['id']); return $responses->createResponse(204);}سریع پاسخ دهید: تحویلی که ظرف 5 ثانیه پاسخی نگیرد شکستخورده به حساب میآید و بعداً دوباره فرستاده میشود، پس کار را به یک صف بسپارید و با یک 2xx پاسخ دهید. کنترلرهای Laravel و Symfony، همراه با سپردن کار به صف، در صفحهٔ فریمورکها هستند.
OpenEmail\Constants\WebhookSignatureHeaders سه سرآیندی را که هر تحویل دارد نام میبرد: X-OpenEmail-Signature، X-OpenEmail-Event با نوع رویداد، و X-OpenEmail-Delivery با شناسهٔ آن، همان idی که در بدنه است. این شناسه در هر تلاش دوباره و بازپخش رویداد یکسان میماند، پس همان است که باید ذخیره کنید تا از رویدادهایی که پیشتر رسیدگی کردهاید بگذرید.
در حین چرخاندن secret
webhooks->rotateSecret هیچ پنجرهٔ همپوشانی ندارد: تحویلها از همان لحظهای که برمیگردد با secret تازه امضا میشوند. نخست گیرندهای را مستقر کنید که هر دو secret را بپذیرد، بچرخانید، secret تازه را جایی ذخیره کنید که گیرنده آن را میخواند، سپس secret قدیمی را کنار بگذارید.
use OpenEmail\Exception\WebhookSignatureException;use OpenEmail\OpenEmail; function verifyDelivery(string $payload, mixed $headers): array{ $next = getenv('OPENEMAIL_WEBHOOK_SECRET_NEXT'); try { return OpenEmail::verifyWebhookSignature($payload, $headers, (string) getenv('OPENEMAIL_WEBHOOK_SECRET')); } catch (WebhookSignatureException $error) { if (!is_string($next) || $next === '') { throw $error; } return OpenEmail::verifyWebhookSignature($payload, $headers, $next); }}webhooks->test یک رویداد ساختگیِ امضاشده میفرستد، پس پس از چرخاندن فراخوانیاش کنید تا پیش از برداشتن secret قدیمی ثابت شود که secret تازه درست راستیآزمایی میشود.