user_agent.rb 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  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, false)
  253. http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  254. end
  255. end
  256. # http.set_debug_output($stdout) if options[:debug]
  257. http
  258. end
  259. def self.set_basic_auth(request, options)
  260. # http basic auth (if needed)
  261. if options[:user].present? && options[:password].present?
  262. request.basic_auth options[:user], options[:password]
  263. end
  264. request
  265. end
  266. def self.parse_uri(url, params = {})
  267. uri = URI.parse(url)
  268. uri.query = [uri.query, URI.encode_www_form(params)].join('&') if params.present?
  269. uri
  270. end
  271. def self.set_params(request, params, options)
  272. if options[:json]
  273. if !request.is_a?(Net::HTTP::Get) # GET requests pass params in query, see 'parse_uri'.
  274. request.add_field('Content-Type', 'application/json; charset=utf-8')
  275. if params.present?
  276. request.body = params.to_json
  277. end
  278. end
  279. elsif params.present?
  280. request.set_form_data(params)
  281. end
  282. request
  283. end
  284. def self.set_headers(request, options)
  285. defaults = { 'User-Agent' => __('Zammad User Agent') }
  286. headers = defaults.merge(options.fetch(:headers, {}))
  287. headers.each do |header, value|
  288. request[header] = value
  289. end
  290. request
  291. end
  292. def self.set_signature(request, options)
  293. return request if options[:signature_token].blank?
  294. return request if request.body.blank?
  295. signature = OpenSSL::HMAC.hexdigest('sha1', options[:signature_token], request.body)
  296. request['X-Hub-Signature'] = "sha1=#{signature}"
  297. request
  298. end
  299. def self.log(url, request, response, options)
  300. return if !options[:log]
  301. # request
  302. request_data = {
  303. content: '',
  304. content_type: request['Content-Type'],
  305. content_encoding: request['Content-Encoding'],
  306. source: request['User-Agent'] || request['Server'],
  307. }
  308. request.each_header do |key, value|
  309. request_data[:content] += "#{key}: #{value}\n"
  310. end
  311. body = request.body
  312. if body
  313. request_data[:content] += "\n#{body}"
  314. end
  315. # response
  316. response_data = {
  317. code: 0,
  318. content: '',
  319. content_type: nil,
  320. content_encoding: nil,
  321. source: nil,
  322. }
  323. if response
  324. response_data[:code] = response.code
  325. response_data[:content_type] = response['Content-Type']
  326. response_data[:content_encoding] = response['Content-Encoding']
  327. response_data[:source] = response['User-Agent'] || response['Server']
  328. response.each_header do |key, value|
  329. response_data[:content] += "#{key}: #{value}\n"
  330. end
  331. body = response.body
  332. if body
  333. response_data[:content] += "\n#{body}"
  334. end
  335. end
  336. record = {
  337. direction: 'out',
  338. facility: options[:log][:facility],
  339. url: url,
  340. status: response_data[:code],
  341. ip: nil,
  342. request: request_data,
  343. response: response_data,
  344. method: request.method,
  345. }
  346. HttpLog.create(record)
  347. end
  348. def self.process(request, response, uri, count, params, options) # rubocop:disable Metrics/ParameterLists
  349. log(uri.to_s, request, response, options)
  350. if !response
  351. return Result.new(
  352. error: "Can't connect to #{uri}, got no response!",
  353. success: false,
  354. code: 0,
  355. )
  356. end
  357. case response
  358. when Net::HTTPNotFound
  359. return Result.new(
  360. error: "No such file #{uri}, 404!",
  361. success: false,
  362. code: response.code,
  363. header: response.each_header.to_h,
  364. )
  365. when Net::HTTPClientError
  366. return Result.new(
  367. error: "Client Error: #{response.inspect}!",
  368. success: false,
  369. code: response.code,
  370. body: response.body,
  371. header: response.each_header.to_h,
  372. )
  373. when Net::HTTPInternalServerError
  374. return Result.new(
  375. error: "Server Error: #{response.inspect}!",
  376. success: false,
  377. code: response.code,
  378. header: response.each_header.to_h,
  379. )
  380. when Net::HTTPRedirection
  381. raise __('Too many redirections for the original URL, halting.') if count <= 0
  382. url = response['location']
  383. return get(url, params, options, count - 1)
  384. when Net::HTTPSuccess
  385. data = nil
  386. if options[:json] && !options[:jsonParseDisable] && response.body
  387. data = JSON.parse(response.body)
  388. end
  389. return Result.new(
  390. data: data,
  391. body: response.body,
  392. content_type: response['Content-Type'],
  393. success: true,
  394. code: response.code,
  395. header: response.each_header.to_h,
  396. )
  397. end
  398. raise "Unable to process http call '#{response.inspect}'"
  399. end
  400. def self.ftp(uri, options)
  401. host = uri.host
  402. filename = File.basename(uri.path)
  403. remote_dir = File.dirname(uri.path)
  404. temp_file = Tempfile.new("download-#{filename}")
  405. temp_file.binmode
  406. begin
  407. Net::FTP.open(host) do |ftp|
  408. ftp.passive = true
  409. if options[:user] && options[:password]
  410. ftp.login(options[:user], options[:password])
  411. else
  412. ftp.login
  413. end
  414. ftp.chdir(remote_dir) if remote_dir != '.'
  415. begin
  416. ftp.getbinaryfile(filename, temp_file)
  417. rescue => e
  418. return Result.new(
  419. error: e.inspect,
  420. success: false,
  421. code: '550',
  422. )
  423. end
  424. end
  425. rescue => e
  426. return Result.new(
  427. error: e.inspect,
  428. success: false,
  429. code: 0,
  430. )
  431. end
  432. contents = temp_file.read
  433. temp_file.close
  434. Result.new(
  435. body: contents,
  436. success: true,
  437. code: '200',
  438. )
  439. end
  440. def self.handled_open_timeout(tries)
  441. tries ||= 1
  442. tries.times do |index|
  443. yield
  444. rescue Net::OpenTimeout
  445. raise if (index + 1) == tries
  446. end
  447. end
  448. class Result
  449. attr_reader :error, :body, :data, :code, :content_type, :header
  450. def initialize(options)
  451. @success = options[:success]
  452. @body = options[:body]
  453. @data = options[:data]
  454. @code = options[:code]
  455. @content_type = options[:content_type]
  456. @error = options[:error]
  457. @header = options[:header]
  458. end
  459. def success?
  460. return true if @success
  461. false
  462. end
  463. end
  464. end