user_agent.rb 13 KB

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