user_agent.rb 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  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. proxy_no = options['proxy_no'] || Setting.get('proxy_no') || ''
  204. proxy_no = proxy_no.split(',').map(&:strip) || []
  205. proxy_no.push('localhost', '127.0.0.1', '::1')
  206. if proxy.present? && !proxy_no.include?(uri.host.downcase)
  207. if proxy =~ /^(.+?):(.+?)$/
  208. proxy_host = $1
  209. proxy_port = $2
  210. else
  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 =~ /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.empty?
  245. request.body = params.to_json
  246. end
  247. elsif !params.empty?
  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. )
  325. when Net::HTTPInternalServerError
  326. return Result.new(
  327. error: "Server Error: #{response.inspect}!",
  328. success: false,
  329. code: response.code,
  330. )
  331. when Net::HTTPRedirection
  332. raise 'Too many redirections for the original URL, halting.' if count <= 0
  333. url = response['location']
  334. return get(url, params, options, count - 1)
  335. when Net::HTTPOK
  336. data = nil
  337. if options[:json] && !options[:jsonParseDisable] && response.body
  338. data = JSON.parse(response.body)
  339. end
  340. return Result.new(
  341. data: data,
  342. body: response.body,
  343. content_type: response['Content-Type'],
  344. success: true,
  345. code: response.code,
  346. )
  347. when Net::HTTPCreated
  348. data = nil
  349. if options[:json] && !options[:jsonParseDisable] && response.body
  350. data = JSON.parse(response.body)
  351. end
  352. return Result.new(
  353. data: data,
  354. body: response.body,
  355. content_type: response['Content-Type'],
  356. success: true,
  357. code: response.code,
  358. )
  359. end
  360. raise "Unable to process http call '#{response.inspect}'"
  361. end
  362. def self.ftp(uri, options)
  363. host = uri.host
  364. filename = File.basename(uri.path)
  365. remote_dir = File.dirname(uri.path)
  366. temp_file = Tempfile.new("download-#{filename}")
  367. temp_file.binmode
  368. begin
  369. Net::FTP.open(host) do |ftp|
  370. ftp.passive = true
  371. if options[:user] && options[:password]
  372. ftp.login(options[:user], options[:password])
  373. else
  374. ftp.login
  375. end
  376. ftp.chdir(remote_dir) unless remote_dir == '.'
  377. begin
  378. ftp.getbinaryfile(filename, temp_file)
  379. rescue => e
  380. return Result.new(
  381. error: e.inspect,
  382. success: false,
  383. code: '550',
  384. )
  385. end
  386. end
  387. rescue => e
  388. return Result.new(
  389. error: e.inspect,
  390. success: false,
  391. code: 0,
  392. )
  393. end
  394. contents = temp_file.read
  395. temp_file.close
  396. Result.new(
  397. body: contents,
  398. success: true,
  399. code: '200',
  400. )
  401. end
  402. class Result
  403. attr_reader :error
  404. attr_reader :body
  405. attr_reader :data
  406. attr_reader :code
  407. attr_reader :content_type
  408. def initialize(options)
  409. @success = options[:success]
  410. @body = options[:body]
  411. @data = options[:data]
  412. @code = options[:code]
  413. @content_type = options[:content_type]
  414. @error = options[:error]
  415. end
  416. def success?
  417. return true if @success
  418. false
  419. end
  420. end
  421. end