user_agent.rb 13 KB

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