تأیید یک تحویل
`OpenEmail.verify_webhook_signature`: با زمان ثابت، با پنجرهٔ بازپخش، و با بازگرداندن رویداد تجزیهشده.
در یک هندلر درخواست
URL یک وبهوک عمومی است. هر چیزی در اینترنت میتواند JSONی با شکل درست به آن POST کند، پس هندلری که type رویداد را بدون بررسی امضا بخواند، یک API نوشتنِ باز است.
class OpenEmailWebhooksController < ActionController::API def create event = OpenEmail.verify_webhook_signature( payload: request.raw_post, headers: request.headers, secret: ENV.fetch("OPENEMAIL_WEBHOOK_SECRET"), tolerance_seconds: 300 ) Rails.logger.info("#{event[:type]} #{event[:data]}") head :no_content rescue OpenEmail::WebhookSignatureError head :bad_request endendبدنهٔ «خام» را بهصورت یک String بدهید. تجزیه و سریالسازی دوباره، ترتیب کلیدها و فاصلهها را تغییر میدهد و امضا مطابقت نمیکند، و به همین دلیل یک Hash، مانند params در Rails، بهجای بررسی شدن ArgumentError را raise میکند. headers: یک Hash با نام سرآیند با هر بزرگی و کوچکی حروف، یک env در Rack، که سرآیند در آن به شکل HTTP_X_OPENEMAIL_SIGNATURE میرسد، یا request.headers در Rails را میگیرد. مقداری که Array باشد از نخستین عنصرش خوانده میشود.
دو چیزی که این متد رسیدگی میکند و بررسیِ دستساز معمولاً نه: MAC را در زمان ثابت مقایسه میکند، تا پیشوند درست با زمانسنجی بازیابی نشود، و تحویلی را که در هر دو جهت بیش از tolerance_seconds: فاصله داشته باشد رد میکند، که پنج دقیقه است مگر چیز دیگری بگویید، تا درخواستی که شنود شده تا ابد قابل بازپخش نباشد. tolerance_seconds: 0 بررسی بازپخش را خاموش میکند. هر دو باگ بیصدا هستند. هندلری که هرکدام را داشته باشد از همهٔ آزمونهایی که به فکرتان برسد سربلند بیرون میآید.
در هر شکستی OpenEmail::WebhookSignatureError را raise میکند: نبودن سرآیند X-OpenEmail-Signature، سرآیندی که به شکل t=<seconds>,v1=<hex> نباشد، مهر زمانیای بیرون از پنجره، یا امضایی که مطابقت نکند. در صورت موفقیت، بدنه را تجزیهشده به یک Hash با کلیدهای Symbol برمیگرداند، id، type، createdAt و data، پس JSON.parse دومی نیست که اشتباه انجام شود.
نبودن یا خالی بودن secret: بهجای آن ArgumentError را raise میکند، چون این اشتباهی در پیکربندی شماست نه تحویلی بد. فقط OpenEmail::WebhookSignatureError را rescue کنید و 400 پاسخ دهید، تا سروری که بد پیکربندی شده 500 پاسخ دهد و تحویل پس از رفع مشکل دوباره امتحان شود.
از OpenSSL کتابخانهٔ استاندارد استفاده میکند که gem از پیش بارگذاریاش کرده، پس به چیز دیگری نیاز ندارد.
در یک برنامهٔ Rack یا Sinatra
بیرون از Rails، خودِ env در Rack را بهعنوان headers: بدهید و بدنهٔ خام را از درخواست بخوانید.
require "openemail"require "rack" webhook = lambda do |env| event = OpenEmail.verify_webhook_signature( payload: Rack::Request.new(env).body.read, headers: env, secret: ENV.fetch("OPENEMAIL_WEBHOOK_SECRET") ) warn "#{event[:type]} #{event[:id]}" [204, {}, []]rescue OpenEmail::WebhookSignatureError [400, {"content-type" => "text/plain"}, ["bad signature"]]end run webhookنسخهٔ Rack یک config.ru کامل است. در هر دو سریع پاسخ دهید: تحویلی که ظرف 5 ثانیه پاسخی نگیرد شکستخورده به حساب میآید و بعداً دوباره فرستاده میشود، پس کار را به یک کار پسزمینه بسپارید و با یک 2xx پاسخ دهید.
OpenEmail::WEBHOOK_SIGNATURE_HEADERS سه سرآیندی را که هر تحویل دارد نام میبرد: X-OpenEmail-Signature، X-OpenEmail-Event با نوع رویداد، و X-OpenEmail-Delivery با شناسهٔ آن، همان idی که در بدنه است. این شناسه در هر تلاش دوباره و بازپخش رویداد یکسان میماند، پس همان است که باید ذخیره کنید تا از رویدادهایی که پیشتر رسیدگی کردهاید بگذرید.
در حین چرخاندن secret
webhooks.rotate_secret هیچ پنجرهٔ همپوشانی ندارد: تحویلها از همان لحظهای که برمیگردد با secret تازه امضا میشوند. نخست گیرندهای را مستقر کنید که هر دو secret را بپذیرد، بچرخانید، secret تازه را جایی ذخیره کنید که گیرنده آن را میخواند، سپس secret قدیمی را کنار بگذارید.
def verify_delivery(payload, headers) secrets = [ENV.fetch("OPENEMAIL_WEBHOOK_SECRET"), ENV["OPENEMAIL_WEBHOOK_SECRET_NEXT"]].compact secrets.each_with_index do |secret, index| return OpenEmail.verify_webhook_signature(payload:, headers:, secret:) rescue OpenEmail::WebhookSignatureError raise if index == secrets.size - 1 endendwebhooks.test یک رویداد ساختگیِ امضاشده میفرستد، پس پس از چرخاندن فراخوانیاش کنید تا پیش از برداشتن secret قدیمی ثابت شود که secret تازه درست راستیآزمایی میشود.