2018-11-18 11:00:15 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
class WebHookService
|
|
|
|
class InternalErrorResponse
|
2020-03-13 15:44:24 +05:30
|
|
|
ERROR_MESSAGE = 'internal error'
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
attr_reader :body, :headers, :code
|
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
def success?
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
|
|
|
def redirection?
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
|
|
|
def internal_server_error?
|
|
|
|
true
|
|
|
|
end
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
def initialize
|
2018-03-26 14:24:53 +05:30
|
|
|
@headers = Gitlab::HTTP::Response::Headers.new({})
|
2017-09-10 17:25:29 +05:30
|
|
|
@body = ''
|
2020-03-13 15:44:24 +05:30
|
|
|
@code = ERROR_MESSAGE
|
2017-09-10 17:25:29 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-10-24 23:57:45 +05:30
|
|
|
REQUEST_BODY_SIZE_LIMIT = 25.megabytes
|
2020-03-07 23:17:34 +05:30
|
|
|
GITLAB_EVENT_HEADER = 'X-Gitlab-Event'
|
2021-06-08 01:23:25 +05:30
|
|
|
MAX_FAILURES = 100
|
2020-03-07 23:17:34 +05:30
|
|
|
|
2018-03-26 14:24:53 +05:30
|
|
|
attr_accessor :hook, :data, :hook_name, :request_options
|
2017-09-10 17:25:29 +05:30
|
|
|
|
2020-03-07 23:17:34 +05:30
|
|
|
def self.hook_to_event(hook_name)
|
|
|
|
hook_name.to_s.singularize.titleize
|
|
|
|
end
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
def initialize(hook, data, hook_name)
|
|
|
|
@hook = hook
|
|
|
|
@data = data
|
2018-03-17 18:26:18 +05:30
|
|
|
@hook_name = hook_name.to_s
|
2019-10-12 21:52:04 +05:30
|
|
|
@request_options = {
|
|
|
|
timeout: Gitlab.config.gitlab.webhook_timeout,
|
2021-07-02 01:05:55 +05:30
|
|
|
use_read_total_timeout: true,
|
2019-10-12 21:52:04 +05:30
|
|
|
allow_local_requests: hook.allow_local_requests?
|
|
|
|
}
|
2017-09-10 17:25:29 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
def execute
|
2021-06-08 01:23:25 +05:30
|
|
|
return { status: :error, message: 'Hook disabled' } unless hook.executable?
|
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
start_time = Gitlab::Metrics::System.monotonic_time
|
2017-09-10 17:25:29 +05:30
|
|
|
|
|
|
|
response = if parsed_url.userinfo.blank?
|
|
|
|
make_request(hook.url)
|
|
|
|
else
|
|
|
|
make_request_with_auth
|
|
|
|
end
|
|
|
|
|
|
|
|
log_execution(
|
|
|
|
trigger: hook_name,
|
|
|
|
url: hook.url,
|
|
|
|
request_data: data,
|
|
|
|
response: response,
|
2018-12-13 13:39:08 +05:30
|
|
|
execution_duration: Gitlab::Metrics::System.monotonic_time - start_time
|
2017-09-10 17:25:29 +05:30
|
|
|
)
|
|
|
|
|
|
|
|
{
|
|
|
|
status: :success,
|
|
|
|
http_status: response.code,
|
2021-07-02 01:05:55 +05:30
|
|
|
message: response.body
|
2017-09-10 17:25:29 +05:30
|
|
|
}
|
2021-03-08 18:12:59 +05:30
|
|
|
rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH,
|
|
|
|
Net::OpenTimeout, Net::ReadTimeout, Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep,
|
|
|
|
Gitlab::Json::LimitedEncoder::LimitExceeded, URI::InvalidURIError => e
|
2020-10-24 23:57:45 +05:30
|
|
|
execution_duration = Gitlab::Metrics::System.monotonic_time - start_time
|
2017-09-10 17:25:29 +05:30
|
|
|
log_execution(
|
|
|
|
trigger: hook_name,
|
|
|
|
url: hook.url,
|
|
|
|
request_data: data,
|
|
|
|
response: InternalErrorResponse.new,
|
2020-10-24 23:57:45 +05:30
|
|
|
execution_duration: execution_duration,
|
2017-09-10 17:25:29 +05:30
|
|
|
error_message: e.to_s
|
|
|
|
)
|
|
|
|
|
2020-10-24 23:57:45 +05:30
|
|
|
Gitlab::AppLogger.error("WebHook Error after #{execution_duration.to_i.seconds}s => #{e}")
|
2017-09-10 17:25:29 +05:30
|
|
|
|
|
|
|
{
|
|
|
|
status: :error,
|
|
|
|
message: e.to_s
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
def async_execute
|
2021-06-08 01:23:25 +05:30
|
|
|
if rate_limited?(hook)
|
|
|
|
log_rate_limit(hook)
|
|
|
|
else
|
|
|
|
WebHookWorker.perform_async(hook.id, data, hook_name)
|
|
|
|
end
|
2017-09-10 17:25:29 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def parsed_url
|
|
|
|
@parsed_url ||= URI.parse(hook.url)
|
|
|
|
end
|
|
|
|
|
|
|
|
def make_request(url, basic_auth = false)
|
2018-03-26 14:24:53 +05:30
|
|
|
Gitlab::HTTP.post(url,
|
2020-10-24 23:57:45 +05:30
|
|
|
body: Gitlab::Json::LimitedEncoder.encode(data, limit: REQUEST_BODY_SIZE_LIMIT),
|
2017-09-10 17:25:29 +05:30
|
|
|
headers: build_headers(hook_name),
|
|
|
|
verify: hook.enable_ssl_verification,
|
2018-03-26 14:24:53 +05:30
|
|
|
basic_auth: basic_auth,
|
|
|
|
**request_options)
|
2017-09-10 17:25:29 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
def make_request_with_auth
|
|
|
|
post_url = hook.url.gsub("#{parsed_url.userinfo}@", '')
|
|
|
|
basic_auth = {
|
|
|
|
username: CGI.unescape(parsed_url.user),
|
2018-11-08 19:23:39 +05:30
|
|
|
password: CGI.unescape(parsed_url.password.presence || '')
|
2017-09-10 17:25:29 +05:30
|
|
|
}
|
|
|
|
make_request(post_url, basic_auth)
|
|
|
|
end
|
|
|
|
|
|
|
|
def log_execution(trigger:, url:, request_data:, response:, execution_duration:, error_message: nil)
|
2021-06-08 01:23:25 +05:30
|
|
|
handle_failure(response, hook)
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
WebHookLog.create(
|
|
|
|
web_hook: hook,
|
|
|
|
trigger: trigger,
|
|
|
|
url: url,
|
|
|
|
execution_duration: execution_duration,
|
|
|
|
request_headers: build_headers(hook_name),
|
|
|
|
request_data: request_data,
|
|
|
|
response_headers: format_response_headers(response),
|
|
|
|
response_body: safe_response_body(response),
|
|
|
|
response_status: response.code,
|
|
|
|
internal_error_message: error_message
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
def handle_failure(response, hook)
|
|
|
|
if response.success? || response.redirection?
|
|
|
|
hook.enable!
|
|
|
|
elsif response.internal_server_error?
|
|
|
|
next_backoff = hook.next_backoff
|
|
|
|
hook.update!(disabled_until: next_backoff.from_now, backoff_count: hook.backoff_count + 1)
|
|
|
|
else
|
|
|
|
hook.update!(recent_failures: hook.recent_failures + 1) if hook.recent_failures < MAX_FAILURES
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
def build_headers(hook_name)
|
|
|
|
@headers ||= begin
|
|
|
|
{
|
|
|
|
'Content-Type' => 'application/json',
|
2021-01-29 00:20:46 +05:30
|
|
|
'User-Agent' => "GitLab/#{Gitlab::VERSION}",
|
2020-03-07 23:17:34 +05:30
|
|
|
GITLAB_EVENT_HEADER => self.class.hook_to_event(hook_name)
|
2017-09-10 17:25:29 +05:30
|
|
|
}.tap do |hash|
|
2018-03-17 18:26:18 +05:30
|
|
|
hash['X-Gitlab-Token'] = Gitlab::Utils.remove_line_breaks(hook.token) if hook.token.present?
|
2017-09-10 17:25:29 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Make response headers more stylish
|
|
|
|
# Net::HTTPHeader has downcased hash with arrays: { 'content-type' => ['text/html; charset=utf-8'] }
|
|
|
|
# This method format response to capitalized hash with strings: { 'Content-Type' => 'text/html; charset=utf-8' }
|
|
|
|
def format_response_headers(response)
|
|
|
|
response.headers.each_capitalized.to_h
|
|
|
|
end
|
|
|
|
|
|
|
|
def safe_response_body(response)
|
|
|
|
return '' unless response.body
|
|
|
|
|
|
|
|
response.body.encode('UTF-8', invalid: :replace, undef: :replace, replace: '')
|
|
|
|
end
|
2021-06-08 01:23:25 +05:30
|
|
|
|
|
|
|
def rate_limited?(hook)
|
|
|
|
return false unless Feature.enabled?(:web_hooks_rate_limit, default_enabled: :yaml)
|
|
|
|
return false if rate_limit.nil?
|
|
|
|
|
|
|
|
Gitlab::ApplicationRateLimiter.throttled?(
|
|
|
|
:web_hook_calls,
|
|
|
|
scope: [hook],
|
|
|
|
threshold: rate_limit
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
def rate_limit
|
|
|
|
@rate_limit ||= hook.rate_limit
|
|
|
|
end
|
|
|
|
|
|
|
|
def log_rate_limit(hook)
|
|
|
|
payload = {
|
|
|
|
message: 'Webhook rate limit exceeded',
|
|
|
|
hook_id: hook.id,
|
|
|
|
hook_type: hook.type,
|
|
|
|
hook_name: hook_name
|
|
|
|
}
|
|
|
|
|
|
|
|
Gitlab::AuthLogger.error(payload)
|
|
|
|
|
|
|
|
# Also log into application log for now, so we can use this information
|
|
|
|
# to determine suitable limits for gitlab.com
|
|
|
|
Gitlab::AppLogger.error(payload)
|
|
|
|
end
|
2017-09-10 17:25:29 +05:30
|
|
|
end
|