webhook_agent.rb 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. module Agents
  2. class WebhookAgent < Agent
  3. include WebRequestConcern
  4. cannot_be_scheduled!
  5. cannot_receive_events!
  6. description do <<-MD
  7. The Webhook Agent will create events by receiving webhooks from any source. In order to create events with this agent, make a POST request to:
  8. ```
  9. https://#{ENV['DOMAIN']}/users/#{user.id}/web_requests/#{id || ':id'}/#{options['secret'] || ':secret'}
  10. ```
  11. #{'The placeholder symbols above will be replaced by their values once the agent is saved.' unless id}
  12. Options:
  13. * `secret` - A token that the host will provide for authentication.
  14. * `expected_receive_period_in_days` - How often you expect to receive
  15. events this way. Used to determine if the agent is working.
  16. * `payload_path` - JSONPath of the attribute in the POST body to be
  17. used as the Event payload. Set to `.` to return the entire message.
  18. If `payload_path` points to an array, Events will be created for each element.
  19. * `verbs` - Comma-separated list of http verbs your agent will accept.
  20. For example, "post,get" will enable POST and GET requests. Defaults
  21. to "post".
  22. * `response` - The response message to the request. Defaults to 'Event Created'.
  23. * `response_headers` - An object with any custom response headers. (example: `{"Access-Control-Allow-Origin": "*"}`)
  24. * `code` - The response code to the request. Defaults to '201'. If the code is '301' or '302' the request will automatically be redirected to the url defined in "response".
  25. * `recaptcha_secret` - Setting this to a reCAPTCHA "secret" key makes your agent verify incoming requests with reCAPTCHA. Don't forget to embed a reCAPTCHA snippet including your "site" key in the originating form(s).
  26. * `recaptcha_send_remote_addr` - Set this to true if your server is properly configured to set REMOTE_ADDR to the IP address of each visitor (instead of that of a proxy server).
  27. MD
  28. end
  29. event_description do
  30. <<-MD
  31. The event payload is based on the value of the `payload_path` option,
  32. which is set to `#{interpolated['payload_path']}`.
  33. MD
  34. end
  35. def default_options
  36. { "secret" => "supersecretstring",
  37. "expected_receive_period_in_days" => 1,
  38. "payload_path" => "some_key"
  39. }
  40. end
  41. def receive_web_request(params, method, format)
  42. # check the secret
  43. secret = params.delete('secret')
  44. return ["Not Authorized", 401] unless secret == interpolated['secret']
  45. # check the verbs
  46. verbs = (interpolated['verbs'] || 'post').split(/,/).map { |x| x.strip.downcase }.select { |x| x.present? }
  47. return ["Please use #{verbs.join('/').upcase} requests only", 401] unless verbs.include?(method)
  48. # check the code
  49. code = (interpolated['code'].presence || 201).to_i
  50. # check the reCAPTCHA response if required
  51. if recaptcha_secret = interpolated['recaptcha_secret'].presence
  52. recaptcha_response = params.delete('g-recaptcha-response') or
  53. return ["Not Authorized", 401]
  54. parameters = {
  55. secret: recaptcha_secret,
  56. response: recaptcha_response,
  57. }
  58. if boolify(interpolated['recaptcha_send_remote_addr'])
  59. parameters[:remoteip] = request.env['REMOTE_ADDR']
  60. end
  61. begin
  62. response = faraday.post('https://www.google.com/recaptcha/api/siteverify',
  63. parameters)
  64. rescue => e
  65. error "Verification failed: #{e.message}"
  66. return ["Not Authorized", 401]
  67. end
  68. JSON.parse(response.body)['success'] or
  69. return ["Not Authorized", 401]
  70. end
  71. [payload_for(params)].flatten.each do |payload|
  72. create_event(payload: payload)
  73. end
  74. if interpolated['response_headers'].presence
  75. [interpolated(params)['response'] || 'Event Created', code, "text/plain", interpolated['response_headers'].presence]
  76. else
  77. [interpolated(params)['response'] || 'Event Created', code]
  78. end
  79. end
  80. def working?
  81. event_created_within?(interpolated['expected_receive_period_in_days']) && !recent_error_logs?
  82. end
  83. def validate_options
  84. unless options['secret'].present?
  85. errors.add(:base, "Must specify a secret for 'Authenticating' requests")
  86. end
  87. if options['code'].present? && options['code'].to_s !~ /\A\s*(\d+|\{.*)\s*\z/
  88. errors.add(:base, "Must specify a code for request responses")
  89. end
  90. if options['code'].to_s.in?(['301', '302']) && !options['response'].present?
  91. errors.add(:base, "Must specify a url for request redirect")
  92. end
  93. end
  94. def payload_for(params)
  95. Utils.value_at(params, interpolated['payload_path']) || {}
  96. end
  97. end
  98. end