Files
fizzy/app/models/webhook/delivery.rb
T

147 lines
3.6 KiB
Ruby

class Webhook::Delivery < ApplicationRecord
STALE_TRESHOLD = 7.days
USER_AGENT = "fizzy/1.0.0 Webhook"
ENDPOINT_TIMEOUT = 7.seconds
belongs_to :account, default: -> { webhook.account }
belongs_to :webhook
belongs_to :event
store :request, coder: JSON
store :response, coder: JSON
enum :state, %w[ pending in_progress completed errored ].index_by(&:itself), default: :pending
scope :ordered, -> { order created_at: :desc, id: :desc }
scope :stale, -> { where(created_at: ...STALE_TRESHOLD.ago) }
after_create_commit :deliver_later
def self.cleanup
stale.delete_all
end
def deliver_later
Webhook::DeliveryJob.perform_later(self)
end
def deliver
in_progress!
self.request[:headers] = headers
self.response = perform_request
self.state = :completed
save!
webhook.delinquency_tracker.record_delivery_of(self)
rescue
errored!
raise
end
def failed?
(errored? || completed?) && !succeeded?
end
def succeeded?
completed? && response[:error].blank? && response[:code].between?(200, 299)
end
private
def perform_request
if resolved_ip.nil?
{ error: :private_uri }
else
response = http.request(
Net::HTTP::Post.new(uri, headers).tap { |request| request.body = payload }
)
{ code: response.code.to_i }
end
rescue Resolv::ResolvTimeout, Resolv::ResolvError, SocketError
{ error: :dns_lookup_failed }
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ETIMEDOUT
{ error: :connection_timeout }
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ECONNRESET
{ error: :destination_unreachable }
rescue OpenSSL::SSL::SSLError
{ error: :failed_tls }
end
def resolved_ip
return @resolved_ip if defined?(@resolved_ip)
@resolved_ip = SsrfProtection.resolve_public_ip(uri.host)
end
def uri
@uri ||= URI(webhook.url)
end
def http
Net::HTTP.new(uri.host, uri.port).tap do |http|
http.ipaddr = resolved_ip
http.use_ssl = (uri.scheme == "https")
http.open_timeout = ENDPOINT_TIMEOUT
http.read_timeout = ENDPOINT_TIMEOUT
end
end
def headers
{
"User-Agent" => USER_AGENT,
"Content-Type" => content_type,
"X-Webhook-Signature" => signature,
"X-Webhook-Timestamp" => event.created_at.utc.iso8601
}
end
def signature
OpenSSL::HMAC.hexdigest("SHA256", webhook.signing_secret, payload)
end
def content_type
if webhook.for_campfire?
"text/html"
elsif webhook.for_basecamp?
"application/x-www-form-urlencoded"
else
"application/json"
end
end
def payload
@payload ||= if webhook.for_basecamp?
{ content: render_payload(formats: :html) }.to_query
elsif webhook.for_campfire?
render_payload(formats: :html)
elsif webhook.for_slack?
html = render_payload(formats: :html)
{ text: convert_html_to_mrkdwn(html) }.to_json
else
render_payload(formats: :json)
end
end
def render_payload(**options)
webhook.renderer.render(layout: false, template: "webhooks/event", assigns: { event: event }, **options).strip
end
def convert_html_to_mrkdwn(html)
document = Nokogiri::HTML5(html)
document.css("a").each do |a|
a.replace("<#{a["href"].strip}|#{a.text}>") if a["href"].present?
end
document.css("b").each do |b|
b.replace("*#{b.text}*")
end
document.css("i").each do |i|
i.replace("_#{i.text}_")
end
document.text
end
end