web_request_concern.rb 5.0 KB

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