user_agent.rb 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  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. end
  211. if proxy_host.blank? || proxy_port.blank?
  212. raise "Invalid proxy address: #{proxy} - expect e.g. proxy.example.com:3128"
  213. end
  214. proxy_username = options['proxy_username'] || Setting.get('proxy_username')
  215. if proxy_username.blank?
  216. proxy_username = nil
  217. end
  218. proxy_password = options['proxy_password'] || Setting.get('proxy_password')
  219. if proxy_password.blank?
  220. proxy_password = nil
  221. end
  222. http = Net::HTTP::Proxy(proxy_host, proxy_port, proxy_username, proxy_password).new(uri.host, uri.port)
  223. else
  224. http = Net::HTTP.new(uri.host, uri.port)
  225. end
  226. http.open_timeout = options[:open_timeout] || 4
  227. http.read_timeout = options[:read_timeout] || 10
  228. if uri.scheme.match?(/https/i)
  229. http.use_ssl = true
  230. # @TODO verify_mode should be configurable
  231. http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  232. end
  233. http
  234. end
  235. def self.set_basic_auth(request, options)
  236. # http basic auth (if needed)
  237. if options[:user] && options[:user] != '' && options[:password] && options[:password] != ''
  238. request.basic_auth options[:user], options[:password]
  239. end
  240. request
  241. end
  242. def self.set_params(request, params, options)
  243. if options[:json]
  244. request.add_field('Content-Type', 'application/json')
  245. if params.present?
  246. request.body = params.to_json
  247. end
  248. elsif params.present?
  249. request.set_form_data(params)
  250. end
  251. request
  252. end
  253. def self.log(url, request, response, options)
  254. return if !options[:log]
  255. # request
  256. request_data = {
  257. content: '',
  258. content_type: request['Content-Type'],
  259. content_encoding: request['Content-Encoding'],
  260. source: request['User-Agent'] || request['Server'],
  261. }
  262. request.each_header do |key, value|
  263. request_data[:content] += "#{key}: #{value}\n"
  264. end
  265. body = request.body
  266. if body
  267. request_data[:content] += "\n" + body
  268. end
  269. request_data[:content] = request_data[:content].slice(0, 8000)
  270. # response
  271. response_data = {
  272. code: 0,
  273. content: '',
  274. content_type: nil,
  275. content_encoding: nil,
  276. source: nil,
  277. }
  278. if response
  279. response_data[:code] = response.code
  280. response_data[:content_type] = response['Content-Type']
  281. response_data[:content_encoding] = response['Content-Encoding']
  282. response_data[:source] = response['User-Agent'] || response['Server']
  283. response.each_header do |key, value|
  284. response_data[:content] += "#{key}: #{value}\n"
  285. end
  286. body = response.body
  287. if body
  288. response_data[:content] += "\n" + body
  289. end
  290. response_data[:content] = response_data[:content].slice(0, 8000)
  291. end
  292. record = {
  293. direction: 'out',
  294. facility: options[:log][:facility],
  295. url: url,
  296. status: response_data[:code],
  297. ip: nil,
  298. request: request_data,
  299. response: response_data,
  300. method: request.method,
  301. }
  302. HttpLog.create(record)
  303. end
  304. def self.process(request, response, uri, count, params, options) # rubocop:disable Metrics/ParameterLists
  305. log(uri.to_s, request, response, options)
  306. if !response
  307. return Result.new(
  308. error: "Can't connect to #{uri}, got no response!",
  309. success: false,
  310. code: 0,
  311. )
  312. end
  313. case response
  314. when Net::HTTPNotFound
  315. return Result.new(
  316. error: "No such file #{uri}, 404!",
  317. success: false,
  318. code: response.code,
  319. )
  320. when Net::HTTPClientError
  321. return Result.new(
  322. error: "Client Error: #{response.inspect}!",
  323. success: false,
  324. code: response.code,
  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) unless 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