require 'net/https'
module EWS # :nodoc:
# A Transporter is responsible for communicating with the E-xact Web Service in
# whichever dialect is chosen by the user. The available options are:
# :json REST with JSON payload
# :rest REST with XML payload (default)
# :soap SOAP
#
# The Transporter will connect to the service, using SSL if required, and will
# encode Reqests to send to the service, and decode Responses received from the
# service.
#
# Once configured to connect to a particular service, it can be used repeatedly
# to send as many transactions as required.
class Transporter
# Initialize a Transporter.
#
# You can specify the URL you would like the Transporter to connect to, although it defaults
# to https://api.e-xact.com, the location of our transaction processing web service.
#
# You can also specify a hash of options as follows:
# :server_cert the path to the server's certificate
# :issuer_cert the path to the certificate of the issuer of the server certificate
# :transport_type the transport_type for this transporter (defaults to :rest)
#
# The default certificates are those required to connect to https://api.e-xact.com and the
# default transport_type is :rest. The default transport_type can be overridden on a per-transaction
# basis, if you choose to do so, by specifying it as a parameter to the submit method.
def initialize(url = "https://api.e-xact.com/", options = {})
@url = URI.parse(url.gsub(/\/$/,''))
base = File.dirname(__FILE__)
@server_cert = options[:server_cert] || base+"/../../certs/exact.cer"
@issuer_cert = options[:issuer_cert] || base+"/../../certs/equifax_ca.cer"
@transport_type = options[:transport_type] || :json
end
# Submit a transaction request to the server
#
# transaction:: the Request object to encode for transmission to the server
# transport_type:: (optional) the transport type to use for this transaction only. If it is not specified, the Transporter's transport type will be used
def submit(transaction, transport_type = nil)
raise "Request not supplied" if transaction.nil?
return false unless transaction.valid?
transport_type ||= @transport_type
raise "Transport type #{transport_type} is not supported" unless @@transport_types.include? transport_type
transport_details = @@transport_types[transport_type]
request = build_http_request(transaction, transport_type, transport_details[:suffix])
request.basic_auth(transaction.gateway_id, transaction.password)
request.add_field "Accept", transport_details[:content_type]
request.add_field "User-Agent", "Ruby"
request.add_field "Content-type", "#{transport_details[:content_type]}; charset=UTF-8"
response = get_connection.request(request)
case response
when Net::HTTPSuccess then EWS::Transaction::Mapping.send "#{transport_type}_to_response", response.body
else
r = ::EWS::Transaction::Response.new
if(transport_type == :soap)
# we may have a SOAP Fault
r = EWS::Transaction::Mapping.send "#{transport_type}_to_response", response.body
end
# SOAP Fault may already have populated the error_number etc.
unless r.error_number
# populate the error number and description
r.error_number = response.code.to_i
r.error_description = response.message
end
r
end
end
private
def build_http_request(transaction, transport_type, request_suffix)
req = nil
if !transaction.is_find_transaction? or transport_type == :soap
req = Net::HTTP::Post.new(@url.path + "/transaction.#{request_suffix}")
if transport_type == :soap
# add the SOAPAction header
soapaction = (transaction.is_find_transaction?) ? "TransactionInfo" : "SendAndCommit"
req.add_field "soapaction", "http://secure2.e-xact.com/vplug-in/transaction/rpc-enc/#{soapaction}"
end
req.body = EWS::Transaction::Mapping.send "request_to_#{transport_type.to_s}", transaction
else
req = Net::HTTP::Get.new(@url.path + "/transaction/#{transaction.transaction_tag}.#{request_suffix}")
end
req
end
def get_connection
# re-use the connection if it's available
return @connection unless @connection.nil?
@connection = Net::HTTP.new(@url.host, @url.port)
@connection.set_debug_output $stdout if $DEBUG
if @url.scheme == 'https'
@connection.use_ssl = true
@connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
@connection.ca_file = @issuer_cert
@connection.verify_callback = method(:validate_certificate)
end
@connection
end
# from OpenSSL doco:
# peer cert is always at chain[0], root CA at chain[n-1]
# OpenSSL validates signatures, and issuer attributes at each step in the chain; is_ok reflects the status of these checks
def validate_certificate(is_ok, ctx)
# if OpenSSL has flagged an issue, then fail
return false unless is_ok
peer_cert = ctx.chain[0].to_pem
current_cert = ctx.current_cert
# we're not interested in additional checking of the CA certs
return is_ok unless peer_cert == current_cert.to_pem
# check that the peer_cert is the same as the one we expect for this URL
contents = File.open(@server_cert).read
cert = OpenSSL::X509::Certificate.new(contents)
return OpenSSL::Digest::SHA1.new(current_cert.to_der) == OpenSSL::Digest::SHA1.new(cert.to_der)
end
# what transport types we support, and their corresponding suffixes
@@transport_types = {
:rest => {:suffix => "xml", :content_type => "application/xml"},
:json => {:suffix => "json", :content_type => "application/json"},
:soap => {:suffix => "xmlsoap", :content_type => "application/xml"}
}
end
end