user_agent.rb 12 KB

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