summaryrefslogtreecommitdiff
path: root/app/services/web_hook_service.rb
blob: 4241b912d5bab63c7336101827a3606a7e925e1f (plain)
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
class WebHookService
  class InternalErrorResponse
    attr_reader :body, :headers, :code

    def initialize
      @headers = HTTParty::Response::Headers.new({})
      @body = ''
      @code = 'internal error'
    end
  end

  include HTTParty

  # HTTParty timeout
  default_timeout Gitlab.config.gitlab.webhook_timeout

  attr_accessor :hook, :data, :hook_name

  def initialize(hook, data, hook_name)
    @hook = hook
    @data = data
    @hook_name = hook_name
  end

  def execute
    start_time = Time.now

    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: Time.now - start_time
    )

    [response.code, response.to_s]
  rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
    log_execution(
      trigger: hook_name,
      url: hook.url,
      request_data: data,
      response: InternalErrorResponse.new,
      execution_duration: Time.now - start_time,
      error_message: e.to_s
    )

    Rails.logger.error("WebHook Error => #{e}")

    [nil, e.to_s]
  end

  def async_execute
    Sidekiq::Client.enqueue(WebHookWorker, hook.id, data, hook_name)
  end

  private

  def parsed_url
    @parsed_url ||= URI.parse(hook.url)
  end

  def make_request(url, basic_auth = false)
    self.class.post(url,
      body: data.to_json,
      headers: build_headers(hook_name),
      verify: hook.enable_ssl_verification,
      basic_auth: basic_auth)
  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)
    }
    make_request(post_url, basic_auth)
  end

  def log_execution(trigger:, url:, request_data:, response:, execution_duration:, error_message: nil)
    # logging for ServiceHook's is not available
    return if hook.is_a?(ServiceHook)

    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: response.body,
      response_status: response.code,
      internal_error_message: error_message
    )
  end

  def build_headers(hook_name)
    @headers ||= begin
      {
        'Content-Type' => 'application/json',
        'X-Gitlab-Event' => hook_name.singularize.titleize
      }.tap do |hash|
        hash['X-Gitlab-Token'] = 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
end