user_agent.rb 12 KB

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