app/models/webhook.rb
# frozen_string_literal: true
# == Schema Information
#
# Table name: webhooks
#
# id :integer not null, primary key
# namespace_id :integer
# url :string(255)
# username :string(255)
# password :string(255)
# request_method :integer
# content_type :integer
# enabled :boolean default(FALSE)
# created_at :datetime not null
# updated_at :datetime not null
# name :string(255) not null
#
# Indexes
#
# index_webhooks_on_namespace_id (namespace_id)
#
require "base64"
require "typhoeus"
require "securerandom"
require "json"
require "uri"
# A Webhook describes a kind of callback to an endpoint defined by an URL.
# Further parameters are username and password, which are used for basic
# authentication. The parameters request_method and content_type are limitted
# to GET and POST, and application/json and application/x-www-form-urlencoded
# respectively. Webhooks can be enabled or disabled.
# After a webhook has been triggered with the provided parameters, a
# WebhookDelivery object is created.
class Webhook < ApplicationRecord
include PublicActivity::Common
enum request_method: %w[GET POST]
enum content_type: ["application/json", "application/x-www-form-urlencoded"]
belongs_to :namespace
has_many :deliveries, class_name: "WebhookDelivery", dependent: :destroy, inverse_of: "webhook"
has_many :headers, class_name: "WebhookHeader", dependent: :destroy, inverse_of: "webhook"
validates :url, presence: true, http: true
before_destroy :update_activities!
# Handle a push event from the registry. All enabled webhooks of the provided
# namespace are triggered in parallel.
def self.handle_push_event(event)
registry = Registry.find_from_event(event)
return if registry.nil?
namespace, = Namespace.get_from_repository_name(event["target"]["repository"], registry)
return if namespace.nil?
hydra = Typhoeus::Hydra.hydra
namespace.webhooks.each do |webhook|
next unless webhook.enabled
headers = webhook.process_headers
args = {
method: webhook.request_method,
headers: headers,
body: JSON.generate(event),
timeout: 60,
userpwd: webhook.process_auth
}
hydra.queue create_request(webhook, args, headers, event)
end
hydra.run
end
# Pull event is not handled on Webhook yet.
def self.handle_pull_event(event); end
# Handle a delete event from the registry. All enabled webhooks of the provided
# namespace are triggered in parallel.
def self.handle_delete_event(event)
handle_push_event(event)
end
# host returns the host part of the URL. This is useful when wanting a pretty
# representation of a webhook.
def host
_, _, host, = URI.split url
host
end
# process_headers returns a hash containing the webhook's headers.
def process_headers
{ "Content-Type" => content_type }.tap do |result|
headers.each do |header|
result[header.name] = header.value
end
end
end
# process_auth returns a basic auth string if username and password are provided.
def process_auth
return if username.blank? || password.blank?
"#{username}:#{password}"
end
# create_request creates and returns a Request object with the provided arguments.
def self.create_request(webhook, args, headers, event)
request = Typhoeus::Request.new(webhook.url, args)
request.on_complete do |response|
# prevent uuid clash
loop do
@uuid = SecureRandom.uuid
break if WebhookDelivery.find_by(webhook_id: webhook.id, uuid: @uuid).nil?
end
WebhookDelivery.create(
webhook_id: webhook.id,
uuid: @uuid,
status: response.response_code,
request_header: headers.to_s,
request_body: JSON.generate(event),
response_header: response.response_headers,
response_body: response.response_body
)
end
request
end
private_class_method :create_request
private
# Provide useful parameters for the "timeline" when a webhook has been
# removed.
def update_activities!
_, _, host, = URI.split url
PublicActivity::Activity.where(trackable: self).update_all(
parameters: {
namespace_id: namespace.id,
namespace_name: namespace.clean_name,
webhook_url: url,
webhook_host: host,
webhook_name: name
}
)
end
end