user_agent.rb 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  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. http = Net::HTTP.new(uri.host, uri.port)
  203. http.open_timeout = options[:open_timeout] || 4
  204. http.read_timeout = options[:read_timeout] || 10
  205. if uri.scheme =~ /https/i
  206. http.use_ssl = true
  207. # @TODO verify_mode should be configurable
  208. http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  209. end
  210. http
  211. end
  212. def self.set_basic_auth(request, options)
  213. # http basic auth (if needed)
  214. if options[:user] && options[:user] != '' && options[:password] && options[:password] != ''
  215. request.basic_auth options[:user], options[:password]
  216. end
  217. request
  218. end
  219. def self.set_params(request, params, options)
  220. if options[:json]
  221. request.add_field('Content-Type', 'application/json')
  222. if !params.empty?
  223. request.body = params.to_json
  224. end
  225. elsif !params.empty?
  226. request.set_form_data(params)
  227. end
  228. request
  229. end
  230. def self.log(url, request, response, options)
  231. return if !options[:log]
  232. # request
  233. request_data = {
  234. content: '',
  235. content_type: request['Content-Type'],
  236. content_encoding: request['Content-Encoding'],
  237. source: request['User-Agent'] || request['Server'],
  238. }
  239. request.each_header { |key, value|
  240. request_data[:content] += "#{key}: #{value}\n"
  241. }
  242. body = request.body
  243. if body
  244. request_data[:content] += "\n" + body
  245. end
  246. request_data[:content] = request_data[:content].slice(0, 8000)
  247. # response
  248. response_data = {
  249. code: 0,
  250. content: '',
  251. content_type: nil,
  252. content_encoding: nil,
  253. source: nil,
  254. }
  255. if response
  256. response_data[:code] = response.code
  257. response_data[:content_type] = response['Content-Type']
  258. response_data[:content_encoding] = response['Content-Encoding']
  259. response_data[:source] = response['User-Agent'] || response['Server']
  260. response.each_header { |key, value|
  261. response_data[:content] += "#{key}: #{value}\n"
  262. }
  263. body = response.body
  264. if body
  265. response_data[:content] += "\n" + body
  266. end
  267. response_data[:content] = response_data[:content].slice(0, 8000)
  268. end
  269. record = {
  270. direction: 'out',
  271. facility: options[:log][:facility],
  272. url: url,
  273. status: response_data[:code],
  274. ip: nil,
  275. request: request_data,
  276. response: response_data,
  277. method: request.method,
  278. }
  279. HttpLog.create(record)
  280. end
  281. def self.process(request, response, uri, count, params, options) # rubocop:disable Metrics/ParameterLists
  282. log(uri.to_s, request, response, options)
  283. if !response
  284. return Result.new(
  285. error: "Can't connect to #{uri}, got no response!",
  286. success: false,
  287. code: 0,
  288. )
  289. end
  290. case response
  291. when Net::HTTPNotFound
  292. return Result.new(
  293. error: "No such file #{uri}, 404!",
  294. success: false,
  295. code: response.code,
  296. )
  297. when Net::HTTPClientError
  298. return Result.new(
  299. error: "Client Error: #{response.inspect}!",
  300. success: false,
  301. code: response.code,
  302. )
  303. when Net::HTTPInternalServerError
  304. return Result.new(
  305. error: "Server Error: #{response.inspect}!",
  306. success: false,
  307. code: response.code,
  308. )
  309. when Net::HTTPRedirection
  310. raise 'Too many redirections for the original URL, halting.' if count <= 0
  311. url = response['location']
  312. return get(url, params, options, count - 1)
  313. when Net::HTTPOK
  314. data = nil
  315. if options[:json] && !options[:jsonParseDisable] && response.body
  316. data = JSON.parse(response.body)
  317. end
  318. return Result.new(
  319. data: data,
  320. body: response.body,
  321. content_type: response['Content-Type'],
  322. success: true,
  323. code: response.code,
  324. )
  325. when Net::HTTPCreated
  326. data = nil
  327. if options[:json] && !options[:jsonParseDisable] && response.body
  328. data = JSON.parse(response.body)
  329. end
  330. return Result.new(
  331. data: data,
  332. body: response.body,
  333. content_type: response['Content-Type'],
  334. success: true,
  335. code: response.code,
  336. )
  337. end
  338. raise "Unable to process http call '#{response.inspect}'"
  339. end
  340. def self.ftp(uri, options)
  341. host = uri.host
  342. filename = File.basename(uri.path)
  343. remote_dir = File.dirname(uri.path)
  344. temp_file = Tempfile.new("download-#{filename}")
  345. temp_file.binmode
  346. begin
  347. Net::FTP.open(host) do |ftp|
  348. ftp.passive = true
  349. if options[:user] && options[:password]
  350. ftp.login(options[:user], options[:password])
  351. else
  352. ftp.login
  353. end
  354. ftp.chdir(remote_dir) unless remote_dir == '.'
  355. begin
  356. ftp.getbinaryfile(filename, temp_file)
  357. rescue => e
  358. return Result.new(
  359. error: e.inspect,
  360. success: false,
  361. code: '550',
  362. )
  363. end
  364. end
  365. rescue => e
  366. return Result.new(
  367. error: e.inspect,
  368. success: false,
  369. code: 0,
  370. )
  371. end
  372. contents = temp_file.read
  373. temp_file.close
  374. Result.new(
  375. body: contents,
  376. success: true,
  377. code: '200',
  378. )
  379. end
  380. class Result
  381. attr_reader :error
  382. attr_reader :body
  383. attr_reader :data
  384. attr_reader :code
  385. attr_reader :content_type
  386. def initialize(options)
  387. @success = options[:success]
  388. @body = options[:body]
  389. @data = options[:data]
  390. @code = options[:code]
  391. @content_type = options[:content_type]
  392. @error = options[:error]
  393. end
  394. def success?
  395. return true if @success
  396. false
  397. end
  398. end
  399. end