दस्तावेज़ पर जाएँ
Ruby

किसी delivery का सत्यापन

`OpenEmail.verify_webhook_signature`: constant-time, replay विंडो के साथ, और पार्स किया गया इवेंट वापस।

किसी request handler में

वेबहुक URL सार्वजनिक होता है। इंटरनेट पर कुछ भी उस पर सही आकार का JSON POST कर सकता है, इसलिए जो handler सिग्नेचर जाँचे बिना इवेंट का type पढ़ता है वह एक खुला write API है।

app/controllers/open_email_webhooks_controller.rb
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 के रूप में पास करें। पार्स करके दोबारा serialise करने से कुंजियों का क्रम और ख़ाली जगहें बदल जाती हैं, और सिग्नेचर मेल नहीं खाएगा, इसीलिए Hash, जैसे Rails का params, जाँचे जाने के बजाय ArgumentError raise करता है। headers: किसी भी case में हेडर नाम वाला Hash, Rack env, जहाँ हेडर HTTP_X_OPENEMAIL_SIGNATURE के रूप में आता है, या Rails का request.headers लेता है। Array वाला मान उसके पहले element से पढ़ा जाता है।

दो चीज़ें जो यह संभालता है और हाथ से लिखी जाँच आमतौर पर नहीं: यह MAC की तुलना constant time में करता है, ताकि समय नापकर सही prefix न निकाला जा सके, और यह किसी भी दिशा में tolerance_seconds: से पुरानी डिलीवरी अस्वीकार करता है, जब तक आप कुछ और न कहें तब तक पाँच मिनट, ताकि पकड़ी गई रिक्वेस्ट हमेशा के लिए replay न की जा सके। tolerance_seconds: 0 replay जाँच बंद कर देता है। दोनों bug चुपचाप होते हैं। इनमें से किसी एक वाला handler हर वह टेस्ट पास कर लेता है जो आप लिखने की सोचेंगे।

यह हर विफलता पर OpenEmail::WebhookSignatureError raise करता है: ग़ायब X-OpenEmail-Signature हेडर, ऐसा हेडर जो t=<seconds>,v1=<hex> रूप में न हो, विंडो के बाहर का timestamp, या मेल न खाने वाला सिग्नेचर। सफल होने पर यह बॉडी को Symbol कुंजियों वाले Hash में पार्स करके लौटाता है, id, type, createdAt और data, इसलिए ग़लती करने को कोई दूसरा JSON.parse नहीं बचता।

ग़ायब या ख़ाली secret: इसकी जगह ArgumentError raise करता है, क्योंकि यह ख़राब डिलीवरी नहीं, आपके कॉन्फ़िगरेशन की ग़लती है। सिर्फ़ OpenEmail::WebhookSignatureError को rescue करें और 400 जवाब दें, ताकि ग़लत कॉन्फ़िगर किया गया सर्वर 500 जवाब दे और ठीक करने के बाद डिलीवरी फिर आज़माई जाए।

यह मानक लाइब्रेरी का OpenSSL इस्तेमाल करता है, जिसे gem पहले से लोड करता है, इसलिए इसे और कुछ नहीं चाहिए।

Rack या Sinatra ऐप में

Rails के बाहर, Rack env को ही 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, और उसकी id के साथ X-OpenEmail-Delivery, वही id जो बॉडी में है। वह id इवेंट के हर पुनः प्रयास और replay पर एक ही रहती है, इसलिए पहले संभाले गए इवेंट छोड़ने के लिए उसी को सहेजें।

जब secret rotate हो रहा हो

webhooks.rotate_secret में कोई overlap विंडो नहीं है: इसके लौटते ही डिलीवरी नए secret से हस्ताक्षरित होती हैं। पहले ऐसा receiver deploy करें जो दोनों में से कोई भी secret स्वीकार करे, rotate करें, नया secret वहाँ सहेजें जहाँ से receiver उसे पढ़ता है, फिर पुराने को हटा दें।

two_secrets.rb
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  endend

webhooks.test एक हस्ताक्षरित कृत्रिम इवेंट भेजता है, इसलिए पुराने को हटाने से पहले यह साबित करने के लिए कि नया secret सत्यापित होता है, rotation के बाद इसे कॉल करें।