123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- require 'net/http'
- require 'net/https'
- require 'net/ftp'
- class UserAgent
- =begin
- get http/https calls
- result = UserAgent.get('http://host/some_dir/some_file?param1=123')
- result = UserAgent.get(
- 'http://host/some_dir/some_file?param1=123',
- {
- param1: 'some value',
- },
- {
- open_timeout: 4,
- read_timeout: 10,
- verify_ssl: true,
- user: 'http basic auth username',
- password: 'http basic auth password',
- bearer_token: 'bearer token authentication',
- },
- )
- returns
- result.body # as response
- get json object
- result = UserAgent.get(
- 'http://host/some_dir/some_file?param1=123',
- {},
- {
- json: true,
- }
- )
- returns
- result.data # as json object
- =end
- def self.get(url, params = {}, options = {}, count = 10)
- # Any params must be added to the URL for GET requests.
- uri = parse_uri(url, params)
- http = get_http(uri, options)
- # prepare request
- request = Net::HTTP::Get.new(uri)
- # set headers
- request = set_headers(request, options)
- # http basic auth (if needed)
- request = set_basic_auth(request, options)
- # bearer token auth (if needed)
- request = set_bearer_token_auth(request, options)
- # add signature
- request = set_signature(request, options)
- # start http call
- begin
- total_timeout = options[:total_timeout] || 60
- handled_open_timeout(options[:open_socket_tries]) do
- Timeout.timeout(total_timeout) do
- response = http.request(request)
- return process(request, response, uri, count, params, options)
- end
- end
- rescue => e
- log(url, request, nil, options)
- Result.new(
- error: e.inspect,
- success: false,
- code: 0,
- )
- end
- end
- =begin
- post http/https calls
- result = UserAgent.post(
- 'http://host/some_dir/some_file',
- {
- param1: 1,
- param2: 2,
- },
- {
- open_timeout: 4,
- read_timeout: 10,
- verify_ssl: true,
- user: 'http basic auth username',
- password: 'http basic auth password',
- bearer_token: 'bearer token authentication',
- total_timeout: 60,
- },
- )
- returns
- result # result object
- =end
- def self.post(url, params = {}, options = {}, count = 10)
- uri = parse_uri(url)
- http = get_http(uri, options)
- # prepare request
- request = Net::HTTP::Post.new(uri)
- # set headers
- request = set_headers(request, options)
- # set params
- request = set_params(request, params, options)
- # http basic auth (if needed)
- request = set_basic_auth(request, options)
- # bearer token auth (if needed)
- request = set_bearer_token_auth(request, options)
- # add signature
- request = set_signature(request, options)
- # start http call
- begin
- total_timeout = options[:total_timeout] || 60
- handled_open_timeout(options[:open_socket_tries]) do
- Timeout.timeout(total_timeout) do
- response = http.request(request)
- return process(request, response, uri, count, params, options)
- end
- end
- rescue => e
- log(url, request, nil, options)
- Result.new(
- error: e.inspect,
- success: false,
- code: 0,
- )
- end
- end
- =begin
- put http/https calls
- result = UserAgent.put(
- 'http://host/some_dir/some_file',
- {
- param1: 1,
- param2: 2,
- },
- {
- open_timeout: 4,
- read_timeout: 10,
- verify_ssl: true,
- user: 'http basic auth username',
- password: 'http basic auth password',
- bearer_token: 'bearer token authentication',
- },
- )
- returns
- result # result object
- =end
- def self.put(url, params = {}, options = {}, count = 10)
- uri = parse_uri(url)
- http = get_http(uri, options)
- # prepare request
- request = Net::HTTP::Put.new(uri)
- # set headers
- request = set_headers(request, options)
- # set params
- request = set_params(request, params, options)
- # http basic auth (if needed)
- request = set_basic_auth(request, options)
- # bearer token auth (if needed)
- request = set_bearer_token_auth(request, options)
- # add signature
- request = set_signature(request, options)
- # start http call
- begin
- total_timeout = options[:total_timeout] || 60
- handled_open_timeout(options[:open_socket_tries]) do
- Timeout.timeout(total_timeout) do
- response = http.request(request)
- return process(request, response, uri, count, params, options)
- end
- end
- rescue => e
- log(url, request, nil, options)
- Result.new(
- error: e.inspect,
- success: false,
- code: 0,
- )
- end
- end
- =begin
- delete http/https calls
- result = UserAgent.delete(
- 'http://host/some_dir/some_file',
- {
- open_timeout: 4,
- read_timeout: 10,
- verify_ssl: true,
- user: 'http basic auth username',
- password: 'http basic auth password',
- bearer_token: 'bearer token authentication',
- },
- )
- returns
- result # result object
- =end
- def self.delete(url, params = {}, options = {}, count = 10)
- uri = parse_uri(url)
- http = get_http(uri, options)
- # prepare request
- request = Net::HTTP::Delete.new(uri)
- # set headers
- request = set_headers(request, options)
- # http basic auth (if needed)
- request = set_basic_auth(request, options)
- # bearer token auth (if needed)
- request = set_bearer_token_auth(request, options)
- # add signature
- request = set_signature(request, options)
- # start http call
- begin
- total_timeout = options[:total_timeout] || 60
- handled_open_timeout(options[:open_socket_tries]) do
- Timeout.timeout(total_timeout) do
- response = http.request(request)
- return process(request, response, uri, count, params, options)
- end
- end
- rescue => e
- log(url, request, nil, options)
- Result.new(
- error: e.inspect,
- success: false,
- code: 0,
- )
- end
- end
- =begin
- perform get http/https/ftp calls
- result = UserAgent.request('ftp://host/some_dir/some_file.bin')
- result = UserAgent.request('http://host/some_dir/some_file.bin')
- result = UserAgent.request('https://host/some_dir/some_file.bin')
- # get request
- result = UserAgent.request(
- 'http://host/some_dir/some_file?param1=123',
- {
- open_timeout: 4,
- read_timeout: 10,
- },
- )
- returns
- result # result object
- =end
- def self.request(url, options = {})
- uri = parse_uri(url)
- case uri.scheme.downcase
- when %r{ftp}
- ftp(uri, options)
- when %r{http|https}
- get(url, {}, options)
- end
- end
- def self.get_http(uri, options)
- proxy = options['proxy'] || Setting.get('proxy')
- proxy_no = options['proxy_no'] || Setting.get('proxy_no') || ''
- proxy_no = proxy_no.split(',').map(&:strip) || []
- proxy_no.push('localhost', '127.0.0.1', '::1')
- if proxy.present? && proxy_no.exclude?(uri.host.downcase)
- if proxy =~ %r{^(.+?):(.+?)$}
- proxy_host = $1
- proxy_port = $2
- end
- if proxy_host.blank? || proxy_port.blank?
- raise "Invalid proxy address: #{proxy} - expect e.g. proxy.example.com:3128"
- end
- proxy_username = options['proxy_username'] || Setting.get('proxy_username')
- if proxy_username.blank?
- proxy_username = nil
- end
- proxy_password = options['proxy_password'] || Setting.get('proxy_password')
- if proxy_password.blank?
- proxy_password = nil
- end
- http = Net::HTTP::Proxy(proxy_host, proxy_port, proxy_username, proxy_password).new(uri.host, uri.port)
- else
- http = Net::HTTP.new(uri.host, uri.port)
- end
- http.open_timeout = options[:open_timeout] || 4
- http.read_timeout = options[:read_timeout] || 10
- if uri.scheme == 'https'
- http.use_ssl = true
- if options.fetch(:verify_ssl, true)
- Certificate::ApplySSLCertificates.ensure_fresh_ssl_context
- else
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
- end
- end
- # http.set_debug_output($stdout) if options[:debug]
- http
- end
- def self.set_basic_auth(request, options)
- # http basic auth (if needed)
- if options[:user].present? && options[:password].present?
- request.basic_auth options[:user], options[:password]
- end
- request
- end
- def self.set_bearer_token_auth(request, options)
- request.tap do |req|
- next if options[:bearer_token].blank?
- req['Authorization'] = "Bearer #{options[:bearer_token]}"
- end
- end
- def self.parse_uri(url, params = {})
- uri = URI.parse(url)
- uri.query = [uri.query, URI.encode_www_form(params)].join('&') if params.present?
- uri
- end
- def self.set_params(request, params, options)
- if options[:json]
- if !request.is_a?(Net::HTTP::Get) # GET requests pass params in query, see 'parse_uri'.
- request.add_field('Content-Type', 'application/json; charset=utf-8')
- if params.present?
- request.body = params.to_json
- end
- end
- elsif params.present?
- request.set_form_data(params)
- end
- request
- end
- def self.set_headers(request, options)
- defaults = { 'User-Agent' => __('Zammad User Agent') }
- headers = defaults.merge(options.fetch(:headers, {}))
- headers.each do |header, value|
- request[header] = value
- end
- request
- end
- def self.set_signature(request, options)
- return request if options[:signature_token].blank?
- return request if request.body.blank?
- signature = OpenSSL::HMAC.hexdigest('sha1', options[:signature_token], request.body)
- request['X-Hub-Signature'] = "sha1=#{signature}"
- request
- end
- def self.log(url, request, response, options)
- return if !options[:log]
- # request
- request_data = {
- content: '',
- content_type: request['Content-Type'],
- content_encoding: request['Content-Encoding'],
- source: request['User-Agent'] || request['Server'],
- }
- request.each_header do |key, value|
- request_data[:content] += "#{key}: #{value}\n"
- end
- body = request.body
- if body
- request_data[:content] += "\n#{body}"
- end
- # response
- response_data = {
- code: 0,
- content: '',
- content_type: nil,
- content_encoding: nil,
- source: nil,
- }
- if response
- response_data[:code] = response.code
- response_data[:content_type] = response['Content-Type']
- response_data[:content_encoding] = response['Content-Encoding']
- response_data[:source] = response['User-Agent'] || response['Server']
- response.each_header do |key, value|
- response_data[:content] += "#{key}: #{value}\n"
- end
- body = response.body
- if body
- response_data[:content] += "\n#{body}"
- end
- end
- record = {
- direction: 'out',
- facility: options[:log][:facility],
- url: url,
- status: response_data[:code],
- ip: nil,
- request: request_data,
- response: response_data,
- method: request.method,
- }
- HttpLog.create(record)
- end
- def self.process(request, response, uri, count, params, options) # rubocop:disable Metrics/ParameterLists
- log(uri.to_s, request, response, options)
- if !response
- return Result.new(
- error: "Can't connect to #{uri}, got no response!",
- success: false,
- code: 0,
- )
- end
- case response
- when Net::HTTPNotFound
- return Result.new(
- error: "No such file #{uri}, 404!",
- success: false,
- code: response.code,
- body: response.body,
- header: response.each_header.to_h,
- )
- when Net::HTTPClientError
- return Result.new(
- error: "Client Error: #{response.inspect}!",
- success: false,
- code: response.code,
- body: response.body,
- header: response.each_header.to_h,
- )
- when Net::HTTPInternalServerError
- return Result.new(
- error: "Server Error: #{response.inspect}!",
- success: false,
- code: response.code,
- body: response.body,
- header: response.each_header.to_h,
- )
- when Net::HTTPRedirection
- raise __('Too many redirections for the original URL, halting.') if count <= 0
- url = response['location']
- return get(url, params, options, count - 1)
- when Net::HTTPSuccess
- data = nil
- if options[:json] && !options[:jsonParseDisable] && response.body
- data = JSON.parse(response.body)
- end
- return Result.new(
- data: data,
- body: response.body,
- content_type: response['Content-Type'],
- success: true,
- code: response.code,
- header: response.each_header.to_h,
- )
- end
- raise "Unable to process http call '#{response.inspect}'"
- end
- def self.ftp(uri, options)
- host = uri.host
- filename = File.basename(uri.path)
- remote_dir = File.dirname(uri.path)
- temp_file = Tempfile.new("download-#{filename}")
- temp_file.binmode
- begin
- Net::FTP.open(host) do |ftp|
- ftp.passive = true
- if options[:user] && options[:password]
- ftp.login(options[:user], options[:password])
- else
- ftp.login
- end
- ftp.chdir(remote_dir) if remote_dir != '.'
- begin
- ftp.getbinaryfile(filename, temp_file)
- rescue => e
- return Result.new(
- error: e.inspect,
- success: false,
- code: '550',
- )
- end
- end
- rescue => e
- return Result.new(
- error: e.inspect,
- success: false,
- code: 0,
- )
- end
- contents = temp_file.read
- temp_file.close
- Result.new(
- body: contents,
- success: true,
- code: '200',
- )
- end
- def self.handled_open_timeout(tries)
- tries ||= 1
- tries.times do |index|
- yield
- rescue Net::OpenTimeout
- raise if (index + 1) == tries
- end
- end
- class Result
- attr_reader :error, :body, :data, :code, :content_type, :header
- def initialize(options)
- @success = options[:success]
- @body = options[:body]
- @data = options[:data]
- @code = options[:code]
- @content_type = options[:content_type]
- @error = options[:error]
- @header = options[:header]
- end
- def success?
- return true if @success
- false
- end
- end
- end
|