user_agent.rb 14 KB

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