web_request_concern.rb 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. require 'faraday'
  2. module WebRequestConcern
  3. module DoNotEncoder
  4. def self.encode(params)
  5. params.map do |key, value|
  6. value.nil? ? "#{key}" : "#{key}=#{value}"
  7. end.join('&')
  8. end
  9. def self.decode(val)
  10. [val]
  11. end
  12. end
  13. class CharacterEncoding < Faraday::Middleware
  14. def initialize(app, options = {})
  15. super(app)
  16. @force_encoding = options[:force_encoding]
  17. @default_encoding = options[:default_encoding]
  18. @unzip = options[:unzip]
  19. end
  20. def call(env)
  21. @app.call(env).on_complete do |env|
  22. body = env[:body]
  23. if @unzip == 'gzip'
  24. begin
  25. body.replace(ActiveSupport::Gzip.decompress(body))
  26. rescue Zlib::GzipFile::Error => e
  27. log e.message
  28. end
  29. end
  30. case
  31. when @force_encoding
  32. encoding = @force_encoding
  33. when body.encoding == Encoding::ASCII_8BIT
  34. # Not all Faraday adapters support automatic charset
  35. # detection, so we do that.
  36. case env[:response_headers][:content_type]
  37. when /;\s*charset\s*=\s*([^()<>@,;:\\"\/\[\]?={}\s]+)/i
  38. encoding = begin
  39. Encoding.find($1)
  40. rescue StandardError
  41. @default_encoding
  42. end
  43. when /\A\s*(?:text\/[^\s;]+|application\/(?:[^\s;]+\+)?(?:xml|json))\s*(?:;|\z)/i
  44. encoding = @default_encoding
  45. else
  46. # Never try to transcode a binary content
  47. next
  48. end
  49. # Return body as binary if default_encoding is nil
  50. next if encoding.nil?
  51. end
  52. body.encode!(Encoding::UTF_8, encoding, invalid: :replace, undef: :replace)
  53. end
  54. end
  55. end
  56. Faraday::Response.register_middleware character_encoding: CharacterEncoding
  57. extend ActiveSupport::Concern
  58. def validate_web_request_options!
  59. if options['user_agent'].present?
  60. errors.add(:base, "user_agent must be a string") unless options['user_agent'].is_a?(String)
  61. end
  62. if options['proxy'].present?
  63. errors.add(:base, "proxy must be a string") unless options['proxy'].is_a?(String)
  64. end
  65. if options['disable_ssl_verification'].present? && boolify(options['disable_ssl_verification']).nil?
  66. errors.add(:base, "if provided, disable_ssl_verification must be true or false")
  67. end
  68. unless headers(options['headers']).is_a?(Hash)
  69. errors.add(:base, "if provided, headers must be a hash")
  70. end
  71. begin
  72. basic_auth_credentials(options['basic_auth'])
  73. rescue ArgumentError => e
  74. errors.add(:base, e.message)
  75. end
  76. if (encoding = options['force_encoding']).present?
  77. case encoding
  78. when String
  79. begin
  80. Encoding.find(encoding)
  81. rescue ArgumentError
  82. errors.add(:base, "Unknown encoding: #{encoding.inspect}")
  83. end
  84. else
  85. errors.add(:base, "force_encoding must be a string")
  86. end
  87. end
  88. end
  89. # The default encoding for a text content with no `charset`
  90. # specified in the Content-Type header. Override this and make it
  91. # return nil if you want to detect the encoding on your own.
  92. def default_encoding
  93. Encoding::UTF_8
  94. end
  95. def parse_body?
  96. false
  97. end
  98. def faraday
  99. faraday_options = {
  100. ssl: {
  101. verify: !boolify(options['disable_ssl_verification'])
  102. }
  103. }
  104. @faraday ||= Faraday.new(faraday_options) { |builder|
  105. if parse_body?
  106. builder.response :json
  107. end
  108. builder.response :character_encoding,
  109. force_encoding: interpolated['force_encoding'].presence,
  110. default_encoding:,
  111. unzip: interpolated['unzip'].presence
  112. builder.headers = headers if headers.length > 0
  113. builder.headers[:user_agent] = user_agent
  114. builder.proxy = interpolated['proxy'].presence
  115. unless boolify(interpolated['disable_redirect_follow'])
  116. require 'faraday/follow_redirects'
  117. builder.response :follow_redirects
  118. end
  119. builder.request :multipart
  120. builder.request :url_encoded
  121. if boolify(interpolated['disable_url_encoding'])
  122. builder.options.params_encoder = DoNotEncoder
  123. end
  124. builder.options.timeout = (Delayed::Worker.max_run_time.seconds - 2).to_i
  125. if userinfo = basic_auth_credentials
  126. builder.request :authorization, :basic, *userinfo
  127. end
  128. builder.request :gzip
  129. case backend = faraday_backend
  130. when :typhoeus
  131. require "faraday/#{backend}"
  132. builder.adapter backend, accept_encoding: nil
  133. when :httpclient, :em_http
  134. require "faraday/#{backend}"
  135. builder.adapter backend
  136. end
  137. }
  138. end
  139. def headers(value = interpolated['headers'])
  140. value.presence || {}
  141. end
  142. def basic_auth_credentials(value = interpolated['basic_auth'])
  143. case value
  144. when nil, ''
  145. return nil
  146. when Array
  147. return value if value.size == 2
  148. when /:/
  149. return value.split(/:/, 2)
  150. end
  151. raise ArgumentError.new("bad value for basic_auth: #{value.inspect}")
  152. end
  153. def faraday_backend
  154. ENV.fetch('FARADAY_HTTP_BACKEND') {
  155. case interpolated['backend']
  156. in 'typhoeus' | 'net_http' | 'httpclient' | 'em_http' => backend
  157. backend
  158. else
  159. 'typhoeus'
  160. end
  161. }.to_sym
  162. end
  163. def user_agent
  164. interpolated['user_agent'].presence || self.class.default_user_agent
  165. end
  166. module ClassMethods
  167. def default_user_agent
  168. ENV.fetch('DEFAULT_HTTP_USER_AGENT', "Huginn - https://github.com/huginn/huginn")
  169. end
  170. end
  171. end