user_agent.rb 12 KB

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