debian-mirror-gitlab/app/services/web_hook_service.rb

256 lines
7.4 KiB
Ruby
Raw Normal View History

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
2022-07-23 23:45:48 +05:30
# Response body is for UI display only. It does not make much sense to save
# whatever the receivers throw back at us
RESPONSE_BODY_SIZE_LIMIT = 8.kilobytes
# The headers are for debugging purpose. They are displayed on the UI only.
RESPONSE_HEADERS_COUNT_LIMIT = 50
RESPONSE_HEADERS_SIZE_LIMIT = 1.kilobytes
2020-03-07 23:17:34 +05:30
2018-03-26 14:24:53 +05:30
attr_accessor :hook, :data, :hook_name, :request_options
2021-09-04 01:27:46 +05:30
attr_reader :uniqueness_token
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
2022-04-04 11:22:00 +05:30
def initialize(hook, data, hook_name, uniqueness_token = nil, force: false)
2017-09-10 17:25:29 +05:30
@hook = hook
2022-06-21 17:19:12 +05:30
@data = data.to_h
2018-03-17 18:26:18 +05:30
@hook_name = hook_name.to_s
2021-09-04 01:27:46 +05:30
@uniqueness_token = uniqueness_token
2022-04-04 11:22:00 +05:30
@force = force
2019-10-12 21:52:04 +05:30
@request_options = {
timeout: Gitlab.config.gitlab.webhook_timeout,
allow_local_requests: hook.allow_local_requests?
}
2017-09-10 17:25:29 +05:30
end
2022-04-04 11:22:00 +05:30
def disabled?
!@force && !hook.executable?
end
2017-09-10 17:25:29 +05:30
def execute
2023-01-13 00:05:48 +05:30
return ServiceResponse.error(message: 'Hook disabled') if disabled?
2021-06-08 01:23:25 +05:30
2022-04-04 11:22:00 +05:30
if recursion_blocked?
log_recursion_blocked
2023-01-13 00:05:48 +05:30
return ServiceResponse.error(message: 'Recursive webhook blocked')
2022-04-04 11:22:00 +05:30
end
2022-03-02 08:16:31 +05:30
Gitlab::WebHooks::RecursionDetection.register!(hook)
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?
2022-08-13 15:12:31 +05:30
make_request(parsed_url.to_s)
2017-09-10 17:25:29 +05:30
else
make_request_with_auth
end
log_execution(
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
)
2023-01-13 00:05:48 +05:30
ServiceResponse.success(message: response.body, payload: { http_status: response.code })
2021-09-04 01:27:46 +05:30
rescue *Gitlab::HTTP::HTTP_ERRORS,
2021-03-08 18:12:59 +05:30
Gitlab::Json::LimitedEncoder::LimitExceeded, URI::InvalidURIError => e
2020-10-24 23:57:45 +05:30
execution_duration = Gitlab::Metrics::System.monotonic_time - start_time
2022-08-13 15:12:31 +05:30
error_message = e.to_s
2017-09-10 17:25:29 +05:30
log_execution(
response: InternalErrorResponse.new,
2020-10-24 23:57:45 +05:30
execution_duration: execution_duration,
2022-08-13 15:12:31 +05:30
error_message: error_message
2017-09-10 17:25:29 +05:30
)
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
2023-01-13 00:05:48 +05:30
ServiceResponse.error(message: error_message)
2017-09-10 17:25:29 +05:30
end
def async_execute
2021-09-04 01:27:46 +05:30
Gitlab::ApplicationContext.with_context(hook.application_context) do
2022-07-23 23:45:48 +05:30
break log_rate_limited if rate_limit!
2022-04-04 11:22:00 +05:30
break log_recursion_blocked if recursion_blocked?
2021-09-04 01:27:46 +05:30
2022-04-04 11:22:00 +05:30
params = {
recursion_detection_request_uuid: Gitlab::WebHooks::RecursionDetection::UUID.instance.request_uuid
}.compact
2022-03-02 08:16:31 +05:30
2022-04-04 11:22:00 +05:30
WebHookWorker.perform_async(hook.id, data, hook_name, params)
2021-06-08 01:23:25 +05:30
end
2017-09-10 17:25:29 +05:30
end
private
def parsed_url
2022-08-13 15:12:31 +05:30
@parsed_url ||= URI.parse(hook.interpolated_url)
rescue WebHook::InterpolationError => e
# Behavior-preserving fallback.
Gitlab::ErrorTracking.track_exception(e)
@parsed_url = URI.parse(hook.url)
2017-09-10 17:25:29 +05:30
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),
2022-03-02 08:16:31 +05:30
headers: build_headers,
2017-09-10 17:25:29 +05:30
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
2022-08-13 15:12:31 +05:30
post_url = parsed_url.to_s.gsub("#{parsed_url.userinfo}@", '')
2017-09-10 17:25:29 +05:30
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
2022-06-21 17:19:12 +05:30
def log_execution(response:, execution_duration:, error_message: nil)
2021-09-04 01:27:46 +05:30
category = response_category(response)
log_data = {
2022-06-21 17:19:12 +05:30
trigger: hook_name,
url: hook.url,
2023-04-23 21:23:45 +05:30
interpolated_url: hook.interpolated_url,
2017-09-10 17:25:29 +05:30
execution_duration: execution_duration,
2022-03-02 08:16:31 +05:30
request_headers: build_headers,
2022-06-21 17:19:12 +05:30
request_data: data,
2022-07-23 23:45:48 +05:30
response_headers: safe_response_headers(response),
2017-09-10 17:25:29 +05:30
response_body: safe_response_body(response),
response_status: response.code,
internal_error_message: error_message
2021-09-04 01:27:46 +05:30
}
2022-04-04 11:22:00 +05:30
if @force # executed as part of test - run log-execution inline.
::WebHooks::LogExecutionService.new(hook: hook, log_data: log_data, response_category: category).execute
else
2022-07-23 23:45:48 +05:30
queue_log_execution_with_retry(log_data, category)
end
end
def queue_log_execution_with_retry(log_data, category)
retried = false
begin
::WebHooks::LogExecutionWorker.perform_async(hook.id, log_data, category, uniqueness_token)
rescue Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError
raise if retried
# Strip request data
log_data[:request_data] = ::WebHookLog::OVERSIZE_REQUEST_DATA
retried = true
retry
2022-04-04 11:22:00 +05:30
end
2017-09-10 17:25:29 +05:30
end
2021-09-04 01:27:46 +05:30
def response_category(response)
2021-06-08 01:23:25 +05:30
if response.success? || response.redirection?
2021-09-04 01:27:46 +05:30
:ok
2021-06-08 01:23:25 +05:30
elsif response.internal_server_error?
2021-09-04 01:27:46 +05:30
:error
2021-06-08 01:23:25 +05:30
else
2021-09-04 01:27:46 +05:30
:failed
2021-06-08 01:23:25 +05:30
end
end
2022-03-02 08:16:31 +05:30
def build_headers
2017-09-10 17:25:29 +05:30
@headers ||= begin
2022-03-02 08:16:31 +05:30
headers = {
2017-09-10 17:25:29 +05:30
'Content-Type' => 'application/json',
2021-01-29 00:20:46 +05:30
'User-Agent' => "GitLab/#{Gitlab::VERSION}",
2022-11-25 23:54:43 +05:30
Gitlab::WebHooks::GITLAB_EVENT_HEADER => self.class.hook_to_event(hook_name),
Gitlab::WebHooks::GITLAB_INSTANCE_HEADER => Gitlab.config.gitlab.base_url
2022-03-02 08:16:31 +05:30
}
headers['X-Gitlab-Token'] = Gitlab::Utils.remove_line_breaks(hook.token) if hook.token.present?
headers.merge!(Gitlab::WebHooks::RecursionDetection.header(hook))
2017-09-10 17:25:29 +05:30
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' }
2022-07-23 23:45:48 +05:30
# rubocop:disable Style/HashTransformValues
def safe_response_headers(response)
response.headers.each_capitalized.first(RESPONSE_HEADERS_COUNT_LIMIT).to_h do |header_key, header_value|
[enforce_utf8(header_key), string_size_limit(enforce_utf8(header_value), RESPONSE_HEADERS_SIZE_LIMIT)]
end
2017-09-10 17:25:29 +05:30
end
2022-07-23 23:45:48 +05:30
# rubocop:enable Style/HashTransformValues
2017-09-10 17:25:29 +05:30
def safe_response_body(response)
return '' unless response.body
2022-07-23 23:45:48 +05:30
response_body = enforce_utf8(response.body)
string_size_limit(response_body, RESPONSE_BODY_SIZE_LIMIT)
2017-09-10 17:25:29 +05:30
end
2021-06-08 01:23:25 +05:30
2022-07-23 23:45:48 +05:30
# Increments rate-limit counter.
# Returns true if hook should be rate-limited.
def rate_limit!
Gitlab::WebHooks::RateLimiter.new(hook).rate_limit!
2021-06-08 01:23:25 +05:30
end
2022-03-02 08:16:31 +05:30
def recursion_blocked?
Gitlab::WebHooks::RecursionDetection.block?(hook)
end
2022-07-23 23:45:48 +05:30
def log_rate_limited
log_auth_error('Webhook rate limit exceeded')
2021-06-08 01:23:25 +05:30
end
2022-07-23 23:45:48 +05:30
def log_recursion_blocked
log_auth_error(
'Recursive webhook blocked from executing',
recursion_detection: ::Gitlab::WebHooks::RecursionDetection.to_log(hook)
2021-09-04 01:27:46 +05:30
)
2021-06-08 01:23:25 +05:30
end
2022-03-02 08:16:31 +05:30
2022-07-23 23:45:48 +05:30
def log_auth_error(message, params = {})
2022-03-02 08:16:31 +05:30
Gitlab::AuthLogger.error(
2022-07-23 23:45:48 +05:30
params.merge(
{ message: message, hook_id: hook.id, hook_type: hook.type, hook_name: hook_name },
Gitlab::ApplicationContext.current
)
2022-03-02 08:16:31 +05:30
)
end
2022-07-23 23:45:48 +05:30
def string_size_limit(str, limit)
str.truncate_bytes(limit)
end
def enforce_utf8(str)
Gitlab::EncodingHelper.encode_utf8(str)
end
2017-09-10 17:25:29 +05:30
end