require "savon" require "httpclient" # net/http can't verify cert correctly require "perfect_retry" module Embulk module Input module MarketoApi module Soap class Base attr_reader :endpoint, :wsdl, :user_id, :encryption_key RETRY_TIMEOUT_COUNT = 5 def initialize(endpoint, wsdl, user_id, encryption_key) @endpoint = endpoint @wsdl = wsdl @user_id = user_id @encryption_key = encryption_key end private def savon headers = { 'ns1:AuthenticationHeader' => { "mktowsUserId" => user_id, }.merge(signature) } # NOTE: Do not memoize this to use always fresh signature (avoid 20016 error) # ref. https://jira.talendforge.org/secure/attachmentzip/unzip/167201/49761%5B1%5D/Marketo%20Enterprise%20API%202%200.pdf (41 page) Savon.client( log: false, logger: Embulk.logger, wsdl: wsdl, soap_header: headers, endpoint: endpoint, open_timeout: 90, read_timeout: 90, raise_errors: true, namespace_identifier: :ns1, env_namespace: 'SOAP-ENV', ) end def retryer(retry_options) PerfectRetry.new do |config| config.sleep = proc{|n| retry_options[:retry_initial_wait_sec] * (2 ** (n - 1))} config.limit = retry_options[:retry_limit] config.dont_rescues = [Embulk::ConfigError] config.rescues = [StandardError, Timeout::Error] config.logger = Embulk.logger config.log_level = nil config.raise_original_error = true end end def savon_call(operation, locals={}, retry_options={}) retryer(retry_options).with_retry do catch_unretryable_error do savon.call(operation, locals.merge(advanced_typecasting: false)) end end end def signature timestamp = Time.now.to_s encryption_string = timestamp + user_id digest = OpenSSL::Digest.new('sha1') hashed_signature = OpenSSL::HMAC.hexdigest(digest, encryption_key, encryption_string) { 'requestTimestamp' => timestamp, 'requestSignature' => hashed_signature.to_s } end def catch_unretryable_error(&block) yield rescue Savon::SOAPFault => e Embulk.logger.debug "#{e.class}: #{e.to_hash}" if e.to_hash[:fault][:faultcode].to_str == "SOAP-ENV:Client" raise ConfigError.new e.message end rescue Savon::HTTPError => e # NOTE: Marketo API always return error as HTTP 500 # ref. https://jira.talendforge.org/secure/attachmentzip/unzip/167201/49761%5B1%5D/Marketo%20Enterprise%20API%202%200.pdf Embulk.logger.debug "#{e.class}: #{e.http.body}" soap_code = e.http.body[%r|(.*?)|, 1] soap_message = e.http.body[%r|(.*?)|, 1] case soap_code when "10001", "20011" # Internal Error raise e when "20015" # Request Limit Exceeded raise e when "20024" # 20024 - Concurrent limit exceeded # # Undocumented in a SOAP document but REST document has it # http://developers.marketo.com/documentation/soap/error-codes/ # http://developers.marketo.com/documentation/rest/error-codes/ raise e else # unretryable error such as Authentication Failed, Invalid Request, etc. raise ConfigError.new soap_message end rescue SocketError, ::Java::JavaNet::UnknownHostException, Errno::ECONNREFUSED => e # maybe endpoint/wsdl domain was wrong Embulk.logger.debug "Connection error: endpoint=#{endpoint} wsdl=#{wsdl}" raise ConfigError.new "Connection error: #{e.message} (endpoint is '#{endpoint}')" end end end end end end