1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
|
# frozen_string_literal: true
class WebHookService
class InternalErrorResponse
ERROR_MESSAGE = 'internal error'
attr_reader :body, :headers, :code
def initialize
@headers = Gitlab::HTTP::Response::Headers.new({})
@body = ''
@code = ERROR_MESSAGE
end
end
GITLAB_EVENT_HEADER = 'X-Gitlab-Event'
attr_accessor :hook, :data, :hook_name, :request_options
def self.hook_to_event(hook_name)
hook_name.to_s.singularize.titleize
end
def initialize(hook, data, hook_name)
@hook = hook
@data = data
@hook_name = hook_name.to_s
@request_options = {
timeout: Gitlab.config.gitlab.webhook_timeout,
allow_local_requests: hook.allow_local_requests?
}
end
def execute
start_time = Gitlab::Metrics::System.monotonic_time
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,
execution_duration: Gitlab::Metrics::System.monotonic_time - start_time
)
{
status: :success,
http_status: response.code,
message: response.to_s
}
rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout, Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep => e
log_execution(
trigger: hook_name,
url: hook.url,
request_data: data,
response: InternalErrorResponse.new,
execution_duration: Gitlab::Metrics::System.monotonic_time - start_time,
error_message: e.to_s
)
Gitlab::AppLogger.error("WebHook Error => #{e}")
{
status: :error,
message: e.to_s
}
end
def async_execute
WebHookWorker.perform_async(hook.id, data, hook_name)
end
private
def parsed_url
@parsed_url ||= URI.parse(hook.url)
end
def make_request(url, basic_auth = false)
Gitlab::HTTP.post(url,
body: data.to_json,
headers: build_headers(hook_name),
verify: hook.enable_ssl_verification,
basic_auth: basic_auth,
**request_options)
end
def make_request_with_auth
post_url = hook.url.gsub("#{parsed_url.userinfo}@", '')
basic_auth = {
username: CGI.unescape(parsed_url.user),
password: CGI.unescape(parsed_url.password.presence || '')
}
make_request(post_url, basic_auth)
end
def log_execution(trigger:, url:, request_data:, response:, execution_duration:, error_message: nil)
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
def build_headers(hook_name)
@headers ||= begin
{
'Content-Type' => 'application/json',
GITLAB_EVENT_HEADER => self.class.hook_to_event(hook_name)
}.tap do |hash|
hash['X-Gitlab-Token'] = Gitlab::Utils.remove_line_breaks(hook.token) if hook.token.present?
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
end
|