Class: Sepa::Response

Inherits:
Object
  • Object
show all
Includes:
ActiveModel::Validations, ErrorMessages, Utilities
Defined in:
lib/sepa/response.rb

Overview

Handles soap responses got back from the bank. Bank specific functionality is defined in subclasses. Handles i.e. logic to make sure the response's integrity has not been compromised and has methods to extract content from the response.

Constant Summary

Constants included from ErrorMessages

ErrorMessages::CONTENT_ERROR_MESSAGE, ErrorMessages::CUSTOMER_ID_ERROR_MESSAGE, ErrorMessages::DECRYPTION_ERROR_MESSAGE, ErrorMessages::ENCRYPTION_CERT_ERROR_MESSAGE, ErrorMessages::ENCRYPTION_CERT_REQUEST_ERROR_MESSAGE, ErrorMessages::ENCRYPTION_PRIVATE_KEY_ERROR_MESSAGE, ErrorMessages::ENVIRONMENT_ERROR_MESSAGE, ErrorMessages::FILE_REFERENCE_ERROR_MESSAGE, ErrorMessages::FILE_TYPE_ERROR_MESSAGE, ErrorMessages::HASH_ERROR_MESSAGE, ErrorMessages::NOT_OK_RESPONSE_CODE_ERROR_MESSAGE, ErrorMessages::PIN_ERROR_MESSAGE, ErrorMessages::SIGNATURE_ERROR_MESSAGE, ErrorMessages::SIGNING_CERT_REQUEST_ERROR_MESSAGE, ErrorMessages::STATUS_ERROR_MESSAGE, ErrorMessages::TARGET_ID_ERROR_MESSAGE

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Utilities

#calculate_digest, #canonicalize_exclusively, #canonicalized_node, #cert_request_valid?, #check_validity_against_schema, #csr_to_binary, #decode, #encode, #extract_cert, #format_cert, #format_cert_request, #hmac, #iso_time, #load_body_template, #process_cert_value, #rsa_key, #set_node_id, #validate_signature, #verify_certificate_against_root_certificate, #x509_certificate, #xml_doc

Constructor Details

#initialize(hash = {}) ⇒ Response

Initializes the response with a options hash

Examples:

Possible keys in options hash

{
  response: "something",
  command: :get_user_info,
  error: "I'm error",
  encryption_private_key: OpenSSL::PKey::RSA
}

Parameters:

  • hash (Hash) (defaults to: {})

    Hash of options



47
48
49
50
51
52
53
# File 'lib/sepa/response.rb', line 47

def initialize(hash = {})
  @command                = hash[:command]
  @encryption_private_key = hash[:encryption_private_key]
  @environment            = hash[:environment]
  @error                  = hash[:error]
  @soap                   = hash[:response]
end

Instance Attribute Details

#commandSymbol (readonly)

The command with which the response was initialized

Returns:

  • (Symbol)


23
24
25
# File 'lib/sepa/response.rb', line 23

def command
  @command
end

#environmentSymbol (readonly)

The environment in which the request was sent

Returns:

  • (Symbol)


28
29
30
# File 'lib/sepa/response.rb', line 28

def environment
  @environment
end

#errorString (readonly)

Possible Savon::Error with which the Sepa::Response was initialized

Returns:

  • (String)


18
19
20
# File 'lib/sepa/response.rb', line 18

def error
  @error
end

#soapString (readonly)

The raw soap response in xml

Returns:

  • (String)


13
14
15
# File 'lib/sepa/response.rb', line 13

def soap
  @soap
end

Instance Method Details

#application_response(namespace: BXD) ⇒ String

Gets the application response from the response as an xml document. Makes a call to #extract_application_response to do the extraction.

Returns:

  • (String)

    The application response as a raw xml document



124
125
126
# File 'lib/sepa/response.rb', line 124

def application_response(namespace: BXD)
  @application_response ||= extract_application_response(namespace)
end

#bank_encryption_certificateObject

This method is abstract.


189
# File 'lib/sepa/response.rb', line 189

def bank_encryption_certificate; end

#bank_root_certificateObject

This method is abstract.


195
# File 'lib/sepa/response.rb', line 195

def bank_root_certificate; end

#bank_signing_certificateObject

This method is abstract.


192
# File 'lib/sepa/response.rb', line 192

def bank_signing_certificate; end

#ca_certificateObject

This method is abstract.


204
# File 'lib/sepa/response.rb', line 204

def ca_certificate; end

#certificateOpenSSL::X509::Certificate?

Returns the certificate embedded in the response

Returns:

  • (OpenSSL::X509::Certificate)

    if the certificate is found

  • (nil)

    if the certificate can't be found

Raises:

  • (OpenSSL::X509::CertificateError)

    if the certificate cannot be processed



146
147
148
149
150
# File 'lib/sepa/response.rb', line 146

def certificate
  @certificate ||= begin
    extract_cert(doc, 'BinarySecurityToken', OASIS_SECEXT)
  end
end

#client_errorsObject (private)

Handles errors that have been passed from client



281
282
283
284
# File 'lib/sepa/response.rb', line 281

def client_errors
  client_error = error.to_s
  errors.add(:base, client_error) unless client_error.empty?
end

#contentString

Returns the content of the response according to #command. When command is :download_file, content is returned as a base64 encoded string, when #command is :download_file_list, the content is returned as xml, when #command is :get_user_info, the content is returned as xml and when #command is :upload_file, content is returned as xml

Returns:

  • (String)

    the content as xml or base64 encoded string



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/sepa/response.rb', line 158

def content
  @content ||= begin
    xml = xml_doc(application_response)

    case @command
    when :download_file
      content_node = xml.at('xmlns|Content', xmlns: XML_DATA)
      content_node.content if content_node
    when :download_file_list
      content_node = xml.remove_namespaces!.at('FileDescriptors')
      content_node.to_xml if content_node
    when :get_user_info
      canonicalized_node(xml, XML_DATA, 'UserFileTypes')
    when :upload_file
      signature_node = xml.at('xmlns|Signature', xmlns: DSIG)
      if signature_node
        signature_node.remove
        xml.canonicalize
      end
    end
  end
end

#docNokogiri::XML

Returns the soap of the response as a Nokogiri document

Returns:

  • (Nokogiri::XML)

    The soap as Nokogiri document



58
59
60
# File 'lib/sepa/response.rb', line 58

def doc
  @doc = @soap ? xml_doc(@soap) : xml_doc(@error)
end

#document_must_validate_against_schemaObject (private)

Validates the document against soap schema unless #error is present or command is :get_bank_certificate



262
263
264
265
266
# File 'lib/sepa/response.rb', line 262

def document_must_validate_against_schema
  return if @error || command.to_sym == :get_bank_certificate

  check_validity_against_schema(doc, 'soap.xsd')
end

#error_docNokogiri::XML

Returns the error of the response as a Nokogiri document

Returns:

  • (Nokogiri::XML)

    The error as Nokogiri document



65
66
67
# File 'lib/sepa/response.rb', line 65

def error_doc
  @error_doc ||= xml_doc @error
end

#extract_application_response(namespace) ⇒ String? (private)

Extracts and returns application response from the response

Returns:

  • (String)

    application response as raw xml if it can be found

  • (nil)

    if application response cannot be found



272
273
274
275
276
277
278
# File 'lib/sepa/response.rb', line 272

def extract_application_response(namespace)
  ar_node = doc.at('xmlns|ApplicationResponse', xmlns: namespace)

  return unless ar_node

  decode(ar_node.content)
end

#file_referencesArray

Returns the file references in a download file list response

Returns:

  • (Array)

    File references



131
132
133
134
135
136
137
138
139
# File 'lib/sepa/response.rb', line 131

def file_references
  return unless @command == :download_file_list

  @file_references ||= begin
    xml = xml_doc content
    descriptors = xml.css('FileDescriptor')
    descriptors.map { |descriptor| descriptor.at('FileReference').content }
  end
end

#find_digest_valuesHash (private)

Finds all reference nodes with digest values in the document and returns a hash with uri as the key and digest as the value.

Returns:

  • (Hash)

    hash of digests with reference uri as the key



228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/sepa/response.rb', line 228

def find_digest_values
  references = {}
  reference_nodes = doc.css('xmlns|Reference', xmlns: DSIG)

  reference_nodes.each do |node|
    uri = node.attr('URI')
    digest_value = node.at('xmlns|DigestValue', xmlns: DSIG).content

    references[uri] = digest_value
  end

  references
end

#find_node_by_uri(uri) ⇒ Nokogiri::XML::Node (private)

Find node by it's reference URI in soap header

Parameters:

  • uri (String)

    the node's URI

Returns:

  • (Nokogiri::XML::Node)


290
291
292
# File 'lib/sepa/response.rb', line 290

def find_node_by_uri(uri)
  doc.at("[xmlns|Id='#{uri}']", xmlns: OASIS_UTILITY)
end

#find_nodes_to_verify(references) ⇒ Hash (private)

Finds nodes to verify by comparing their id's to the uris' in the references hash. Then calculates the hashes of those nodes and returns them in a hash

Parameters:

  • references (Hash)

Returns:

  • (Hash)

    hash of calculated digests with reference uri as the key



247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/sepa/response.rb', line 247

def find_nodes_to_verify(references)
  nodes = {}

  references.each do |uri, _digest_value|
    uri = uri.sub(/^#/, '')
    node = find_node_by_uri(uri)

    nodes[uri] = calculate_digest(node)
  end

  nodes
end

#hashes_match?(options = {}) ⇒ false, true

Verifies that all digest values in the response match the actual ones. Takes an optional verbose parameter to show which digests didn't match. The digest embedded in the document are first retrieved with #find_digest_values method and if none are found, false is returned. After this, nodes to calculate hashes from are retrieved and hashes using #find_nodes_to_verify method and after this the calculated digests are compared with the embedded ones. If the all match, true is returned. If some digests failed to verify and verbose parameter was passed, digests that failed to verify are printed to screen and false is returned. Otherwise just false is returned.

Examples:

Options hash

{ verbose: true }

Parameters:

  • options (Hash) (defaults to: {})

Returns:

  • (false)

    if hashes don't match or aren't found

  • (true)

    if hashes match



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/sepa/response.rb', line 83

def hashes_match?(options = {})
  digests = find_digest_values

  return false if digests.empty?

  nodes = find_nodes_to_verify(digests)

  verified_digests = digests.select do |uri, digest|
    uri = uri.sub(/^#/, '')
    digest == nodes[uri]
  end

  return true if digests == verified_digests

  unverified_digests = digests.select do |uri, digest|
    uri = uri.sub(/^#/, '')
    digest != nodes[uri]
  end

  if options[:verbose]
    puts "These digests failed to verify: #{unverified_digests}"
  end

  false
end

#own_encryption_certificateObject

This method is abstract.


198
# File 'lib/sepa/response.rb', line 198

def own_encryption_certificate; end

#own_signing_certificateObject

This method is abstract.


201
# File 'lib/sepa/response.rb', line 201

def own_signing_certificate; end

#response_code(namespace: BXD, node_name: 'ResponseCode') ⇒ String?

Returns the response code of the response

Returns:

  • (String)

    if the response code can be found

  • (nil)

    if the response code cannot be found



210
211
212
# File 'lib/sepa/response.rb', line 210

def response_code(namespace: BXD, node_name: 'ResponseCode')
  (node = doc.at("xmlns|#{node_name}", xmlns: namespace)) && node.content && node.content.rjust(2, '0')
end

#response_code_is_ok?true, false (private)

Checks whether response code in the response is ok. Response code is considered ok if it is "00" or "24".

Returns:

  • (true)

    if response code is ok

  • (false)

    if response code is not ok



331
332
333
334
335
# File 'lib/sepa/response.rb', line 331

def response_code_is_ok?
  return true if %w(00 24).include? response_code

  false
end

#response_text(namespace: BXD, node_name: 'ResponseText') ⇒ String?

Returns the response text of the response

Returns:

  • (String)

    if the response text can be found

  • (nil)

    if the response text cannot be found



218
219
220
# File 'lib/sepa/response.rb', line 218

def response_text(namespace: BXD, node_name: 'ResponseText')
  (node = doc.at("xmlns|#{node_name}", xmlns: namespace)) && node.content
end

#signature_is_valid?true, false

Verifies the signature by extracting the public key from the certificate embedded in the response and verifying the signature value with that. Makes a call to Utilities#validate_signature to do the actual verification. Passes :exclusive to Utilities#validate_signature so that exclusive mode of xml canonicalization is used.

Returns:

  • (true)

    if signature is valid

  • (false)

    if signature fails to verify



116
117
118
# File 'lib/sepa/response.rb', line 116

def signature_is_valid?
  validate_signature(doc, certificate, :exclusive)
end

#to_sString

Returns the raw soap as xml

Returns:

  • (String)


184
185
186
# File 'lib/sepa/response.rb', line 184

def to_s
  @soap
end

#validate_hashesObject (private)

Validates hashes in the response. #hashes_match? must return true for validation to pass. Is not run if #error is present or response code is not ok.



303
304
305
306
307
# File 'lib/sepa/response.rb', line 303

def validate_hashes
  return if @error || !response_code_is_ok? || hashes_match?

  errors.add(:base, HASH_ERROR_MESSAGE)
end

#validate_response_codeObject (private)

Validates response code in response. "00" and "24" are currently considered valid.



295
296
297
298
299
# File 'lib/sepa/response.rb', line 295

def validate_response_code
  return if %w(00 24).include? response_code

  errors.add(:base, response_code: response_code, response_text: response_text)
end

#verify_certificateObject (private)

Validates certificate in the soap. The certificate must be present and signed by the bank's root certificate for the validation to pass. Is not run if #error is present or response code is not ok.



320
321
322
323
324
# File 'lib/sepa/response.rb', line 320

def verify_certificate
  return if @error || !response_code_is_ok? || certificate_is_trusted?

  errors.add(:base, 'The certificate in the response is not trusted')
end

#verify_signatureObject (private)

Validate signature in the response. Validation is not run if #error is present or response is not ok.



311
312
313
314
315
# File 'lib/sepa/response.rb', line 311

def verify_signature
  return if @error || !response_code_is_ok? || signature_is_valid?

  errors.add(:base, SIGNATURE_ERROR_MESSAGE)
end