# 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, 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, 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