user_agent.rb 11 KB

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