user_agent.rb 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. require 'net/http'
  3. require 'net/https'
  4. class UserAgent
  5. # Make HTTP request via GET method
  6. #
  7. # @see .make_connection
  8. def self.get(...)
  9. make_connection(:get, ...)
  10. end
  11. # Make HTTP request via POST method
  12. #
  13. # @see .make_connection
  14. def self.post(...)
  15. make_connection(:post, ...)
  16. end
  17. # Make HTTP request via PATCH method
  18. #
  19. # @see .make_connection
  20. def self.patch(...)
  21. make_connection(:patch, ...)
  22. end
  23. # Make HTTP request via PUT method
  24. #
  25. # @see .make_connection
  26. def self.put(...)
  27. make_connection(:put, ...)
  28. end
  29. # Make HTTP request via DELETE method
  30. #
  31. # @see .make_connection
  32. def self.delete(...)
  33. make_connection(:delete, ...)
  34. end
  35. def self.get_http(uri, options)
  36. proxy = options['proxy'] || Setting.get('proxy')
  37. proxy_no = options['proxy_no'] || Setting.get('proxy_no') || ''
  38. proxy_no = proxy_no.split(',').map(&:strip) || []
  39. proxy_no.push('localhost', '127.0.0.1', '::1')
  40. if proxy.present? && proxy_no.exclude?(uri.host.downcase)
  41. if proxy =~ %r{^(.+?):(.+?)$}
  42. proxy_host = $1
  43. proxy_port = $2
  44. end
  45. if proxy_host.blank? || proxy_port.blank?
  46. raise "Invalid proxy address: #{proxy} - expect e.g. proxy.example.com:3128"
  47. end
  48. proxy_username = options['proxy_username'] || Setting.get('proxy_username')
  49. if proxy_username.blank?
  50. proxy_username = nil
  51. end
  52. proxy_password = options['proxy_password'] || Setting.get('proxy_password')
  53. if proxy_password.blank?
  54. proxy_password = nil
  55. end
  56. http = Net::HTTP::Proxy(proxy_host, proxy_port, proxy_username, proxy_password).new(uri.host, uri.port)
  57. else
  58. http = Net::HTTP.new(uri.host, uri.port)
  59. end
  60. http.open_timeout = options[:open_timeout] || 4
  61. http.read_timeout = options[:read_timeout] || 10
  62. if uri.scheme == 'https'
  63. http.use_ssl = true
  64. if options.fetch(:verify_ssl, true)
  65. Certificate::ApplySSLCertificates.ensure_fresh_ssl_context
  66. else
  67. http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  68. end
  69. end
  70. # http.set_debug_output($stdout) if options[:debug]
  71. http
  72. end
  73. def self.set_basic_auth(request, options)
  74. # http basic auth (if needed)
  75. if options[:user].present? && options[:password].present?
  76. request.basic_auth options[:user], options[:password]
  77. end
  78. request
  79. end
  80. def self.set_bearer_token_auth(request, options)
  81. request.tap do |req|
  82. next if options[:bearer_token].blank?
  83. req['Authorization'] = "Bearer #{options[:bearer_token]}"
  84. end
  85. end
  86. def self.parse_uri(url, params = {}, method = nil)
  87. uri = URI.parse(url)
  88. if method == :get && params.present?
  89. uri.query = [uri.query, URI.encode_www_form(params)].join('&')
  90. end
  91. uri
  92. end
  93. def self.set_params(request, params, options)
  94. if options[:json]
  95. if !request.is_a?(Net::HTTP::Get) # GET requests pass params in query, see 'parse_uri'.
  96. request.add_field('Content-Type', 'application/json; charset=utf-8')
  97. if params.present?
  98. request.body = params.to_json
  99. end
  100. end
  101. elsif params.present?
  102. request.set_form_data(params)
  103. end
  104. request
  105. end
  106. def self.set_headers(request, options)
  107. defaults = { 'User-Agent' => __('Zammad User Agent') }
  108. headers = defaults.merge(options.fetch(:headers, {}))
  109. headers.each do |header, value|
  110. request[header] = value
  111. end
  112. request
  113. end
  114. def self.set_signature(request, options)
  115. return request if options[:signature_token].blank?
  116. return request if request.body.blank?
  117. signature = OpenSSL::HMAC.hexdigest('sha1', options[:signature_token], request.body)
  118. request['X-Hub-Signature'] = "sha1=#{signature}"
  119. request
  120. end
  121. def self.log(url, request, response, options)
  122. return if !options[:log]
  123. # request
  124. request_data = {
  125. content: '',
  126. content_type: request['Content-Type'],
  127. content_encoding: request['Content-Encoding'],
  128. source: request['User-Agent'] || request['Server'],
  129. }
  130. request.each_header do |key, value|
  131. request_data[:content] += "#{key}: #{value}\n"
  132. end
  133. body = request.body
  134. if body
  135. request_data[:content] += "\n#{body}"
  136. end
  137. # response
  138. response_data = {
  139. code: 0,
  140. content: '',
  141. content_type: nil,
  142. content_encoding: nil,
  143. source: nil,
  144. }
  145. if response
  146. response_data[:code] = response.code
  147. response_data[:content_type] = response['Content-Type']
  148. response_data[:content_encoding] = response['Content-Encoding']
  149. response_data[:source] = response['User-Agent'] || response['Server']
  150. response.each_header do |key, value|
  151. response_data[:content] += "#{key}: #{value}\n"
  152. end
  153. body = response.body
  154. if body
  155. response_data[:content] += "\n#{body}"
  156. end
  157. end
  158. record = {
  159. direction: 'out',
  160. facility: options[:log][:facility],
  161. url: url,
  162. status: response_data[:code],
  163. ip: nil,
  164. request: request_data,
  165. response: response_data,
  166. method: request.method,
  167. }
  168. HttpLog.create(record)
  169. end
  170. def self.process(request, response, uri, count, params, options) # rubocop:disable Metrics/ParameterLists
  171. log(uri.to_s, request, response, options)
  172. if !response
  173. return Result.new(
  174. error: "Can't connect to #{uri}, got no response!",
  175. success: false,
  176. code: 0,
  177. )
  178. end
  179. case response
  180. when Net::HTTPNotFound
  181. return Result.new(
  182. error: "No such file #{uri}, 404!",
  183. success: false,
  184. code: response.code,
  185. body: response.body,
  186. header: response.each_header.to_h,
  187. )
  188. when Net::HTTPClientError
  189. return Result.new(
  190. error: "Client Error: #{response.inspect}!",
  191. success: false,
  192. code: response.code,
  193. body: response.body,
  194. header: response.each_header.to_h,
  195. )
  196. when Net::HTTPInternalServerError
  197. return Result.new(
  198. error: "Server Error: #{response.inspect}!",
  199. success: false,
  200. code: response.code,
  201. body: response.body,
  202. header: response.each_header.to_h,
  203. )
  204. when Net::HTTPRedirection
  205. raise __('Too many redirections for the original URL, halting.') if count <= 0
  206. url = response['location']
  207. return get(url, params, options, count - 1)
  208. when Net::HTTPSuccess
  209. data = nil
  210. if options[:json] && !options[:jsonParseDisable] && response.body
  211. data = JSON.parse(response.body)
  212. end
  213. return Result.new(
  214. data: data,
  215. body: response.body,
  216. content_type: response['Content-Type'],
  217. success: true,
  218. code: response.code,
  219. header: response.each_header.to_h,
  220. )
  221. end
  222. raise "Unable to process http call '#{response.inspect}'"
  223. end
  224. def self.handled_open_timeout(tries)
  225. tries ||= 1
  226. tries.times do |index|
  227. yield
  228. rescue Net::OpenTimeout
  229. raise if (index + 1) == tries
  230. end
  231. end
  232. # Base method for making connection
  233. #
  234. # @param method [Symbol] HTTP request method style to use. Must be Net::HTTP::Class
  235. # @param url [String] full URL to request
  236. # @param params [Hash] to add either to GET URL or submit as POST-style data
  237. # @param options [Hash]
  238. # @option options [String] :send_as_raw_body to submit as raw POST-style request data body
  239. # @option options [Integer] :total_timeout of connection
  240. # @option options [Integer] :open_socket_tries count to retry connection
  241. # @option options [Boolean] :verify_ssl
  242. # @option options [Hash] :headers to apply to request
  243. # @option options [String] :signature_token to set as X-Hub-Sighature header
  244. # @option options [Boolean] :json is POST-style data parameters posted as JSON and response shall be parsed as JSON
  245. # @option options [Boolean] :jsonParseDisable disable response parsing as JSON of :json is enabled
  246. # @option options [String] :user for basic authentication
  247. # @option options [String] :password for basic authentication
  248. # @option options [String] :bearer_token for token authentication
  249. # @option options [Hash] :log enable logging
  250. # @option options [String] :proxy address
  251. # @option options [String] :proxy_no list of address to skip proxy for
  252. # @option options [String] :proxy_username
  253. # @option options [String] :proxy_password
  254. # @option options [Integer] :open_timeout
  255. # @option options [Integer] :read_timeout
  256. # @option log [String] :facility is sub-key as in options[:log][:facility] providing name to use when logging in HttpLog
  257. # @param count [Integer] of redirects. Counts towards zero and then aborts
  258. #
  259. # @example
  260. #
  261. # result = UserAgent.make_connection(:get, 'http://host/some_dir/some_file?param1=123',
  262. # { param1: 'some value' } , { option: value })
  263. # result.data => { parsed: 'json' }
  264. #
  265. # @return [Result]
  266. def self.make_connection(method, url, params = {}, options = {}, count = 10)
  267. uri = parse_uri(url, params, method)
  268. http = get_http(uri, options)
  269. # prepare request
  270. request = Net::HTTP.const_get(method.capitalize).new(uri)
  271. # set headers
  272. request = set_headers(request, options)
  273. # set params for non-get requests
  274. if method != :get
  275. request = set_params(request, params, options)
  276. end
  277. # http basic auth (if needed)
  278. request = set_basic_auth(request, options)
  279. # bearer token auth (if needed)
  280. request = set_bearer_token_auth(request, options)
  281. # add signature
  282. request = set_signature(request, options)
  283. # start http call
  284. begin
  285. total_timeout = options[:total_timeout] || 60
  286. handled_open_timeout(options[:open_socket_tries]) do
  287. Timeout.timeout(total_timeout) do
  288. response = if (send_as_raw_body = options[:send_as_raw_body])
  289. http.request(request, send_as_raw_body)
  290. else
  291. http.request(request)
  292. end
  293. return process(request, response, uri, count, params, options)
  294. end
  295. end
  296. rescue => e
  297. log(url, request, nil, options)
  298. Result.new(
  299. error: e.inspect,
  300. success: false,
  301. code: 0,
  302. )
  303. end
  304. end
  305. class Result
  306. attr_reader :error, :body, :data, :code, :content_type, :header
  307. def initialize(options)
  308. @success = options[:success]
  309. @body = options[:body]
  310. @data = options[:data]
  311. @code = options[:code]
  312. @content_type = options[:content_type]
  313. @error = options[:error]
  314. @header = options[:header]
  315. end
  316. def success?
  317. return true if @success
  318. false
  319. end
  320. end
  321. end