sepafm 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,20 +1,20 @@
1
1
  module Sepa
2
2
  class NordeaResponse < Response
3
+ include Utilities
3
4
 
4
- def initialize(response, command: nil)
5
- super
5
+ def own_signing_cert
6
+ application_response = extract_application_response(NORDEA_PKI)
7
+ at = 'xmlns|Certificate > xmlns|Certificate'
8
+ node = Nokogiri::XML(application_response).at(at, xmlns: NORDEA_XML_DATA)
6
9
 
7
- if @command == :get_certificate
8
- @application_response = extract_application_response('http://bxd.fi/CertificateService')
9
- @content = extract_own_cert
10
- end
11
- end
10
+ return unless node
12
11
 
13
- def extract_own_cert
14
- node = Nokogiri::XML(@application_response)
15
- .at('xmlns|Certificate > xmlns|Certificate', xmlns: 'http://filetransfer.nordea.com/xmldata/')
12
+ cert_value = process_cert_value node.content
13
+ cert = x509_certificate cert_value
14
+ cert_plain = cert.to_s
16
15
 
17
- Base64.encode64(OpenSSL::X509::Certificate.new(process_cert_value(node.content)).to_s) if node
16
+ encode cert_plain
18
17
  end
18
+
19
19
  end
20
20
  end
@@ -18,7 +18,7 @@ module Sepa
18
18
  end
19
19
 
20
20
  def set_body_contents
21
- set_node(@template, 'cer|ApplicationRequest', @ar.to_base64)
21
+ set_node(@template, 'cer|ApplicationRequest', @application_request.to_base64)
22
22
  set_node(@template, 'cer|SenderId', @customer_id)
23
23
  set_node(@template, 'cer|RequestId', request_id)
24
24
  set_node(@template, 'cer|Timestamp', iso_time)
@@ -34,7 +34,7 @@ module Sepa
34
34
  end
35
35
 
36
36
  def common_set_body_contents
37
- set_node(@template, 'bxd|ApplicationRequest', @ar.to_base64)
37
+ set_node(@template, 'bxd|ApplicationRequest', @application_request.to_base64)
38
38
  set_node(@template, 'bxd|SenderId', @customer_id)
39
39
  set_node(@template, 'bxd|RequestId', request_id)
40
40
  set_node(@template, 'bxd|Timestamp', iso_time)
@@ -35,6 +35,8 @@ module Sepa
35
35
  def initialize(hash = {})
36
36
  self.attributes hash
37
37
  self.environment ||= 'PRODUCTION'
38
+ self.language ||= 'EN'
39
+ self.status ||= 'NEW'
38
40
  end
39
41
 
40
42
  def bank=(value)
@@ -56,13 +58,26 @@ module Sepa
56
58
 
57
59
  soap = SoapBuilder.new(create_hash).to_xml
58
60
  client = Savon.client(wsdl: wsdl)
59
- response = client.call(command, xml: soap).doc
61
+
62
+ begin
63
+ response = client.call(command, xml: soap)
64
+ response &&= response.to_xml
65
+ rescue Savon::Error => e
66
+ response = nil
67
+ error = e.to_s
68
+ end
69
+
70
+ options = {
71
+ response: response,
72
+ error: error,
73
+ command: command
74
+ }
60
75
 
61
76
  case bank
62
77
  when :nordea
63
- NordeaResponse.new response, command: command
78
+ NordeaResponse.new options
64
79
  when :danske
65
- DanskeResponse.new response, command: command
80
+ DanskeResponse.new options
66
81
  end
67
82
  end
68
83
 
@@ -87,5 +102,27 @@ module Sepa
87
102
  @private_key = OpenSSL::PKey::RSA.new(@private_key) if @private_key
88
103
  end
89
104
 
105
+ # Returns path to WSDL file
106
+ def wsdl
107
+ case bank
108
+ when :nordea
109
+ if command == :get_certificate
110
+ file = "wsdl_nordea_cert.xml"
111
+ else
112
+ file = "wsdl_nordea.xml"
113
+ end
114
+ when :danske
115
+ if [:get_bank_certificate, :create_certificate].include? command
116
+ file = "wsdl_danske_cert.xml"
117
+ else
118
+ file = "wsdl_danske.xml"
119
+ end
120
+ else
121
+ return nil
122
+ end
123
+
124
+ "#{WSDL_PATH}/#{file}"
125
+ end
126
+
90
127
  end
91
128
  end
@@ -3,27 +3,21 @@ module Sepa
3
3
  include ActiveModel::Validations
4
4
  include Utilities
5
5
 
6
- attr_reader :soap, :application_response, :certificate, :content
6
+ attr_reader :soap, :error, :command
7
7
 
8
8
  validates :soap, presence: true
9
9
  validate :validate_document_format
10
10
  validate :document_must_validate_against_schema
11
+ validate :client_errors
11
12
 
12
- GENERIC_COMMANDS = [:get_user_info, :download_file_list, :download_file, :upload_file]
13
-
14
- def initialize(response, command: nil)
15
- @soap = response
16
- @command = command
17
-
18
- # Check if command is one of the generic commands which should behave the same way across
19
- # different banks
20
- if GENERIC_COMMANDS.include? command
21
- xsd = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'
13
+ def initialize(hash = {})
14
+ @soap = hash[:response]
15
+ @command = hash[:command]
16
+ @error = hash[:error]
17
+ end
22
18
 
23
- @application_response = extract_application_response('http://model.bxd.fi')
24
- @certificate = extract_cert(soap, 'BinarySecurityToken', xsd)
25
- @content = extract_content
26
- end
19
+ def doc
20
+ @doc ||= xml_doc @soap
27
21
  end
28
22
 
29
23
  # Verifies that all digest values in the response match the actual ones.
@@ -31,7 +25,7 @@ module Sepa
31
25
  # i.e. verbose: true
32
26
  def hashes_match?(options = {})
33
27
  digests = find_digest_values
34
- nodes = find_nodes_to_verify(soap, digests)
28
+ nodes = find_nodes_to_verify(digests)
35
29
 
36
30
  verified_digests = digests.select do |uri, digest|
37
31
  uri = uri.sub(/^#/, '')
@@ -55,58 +49,76 @@ module Sepa
55
49
  # Verifies the signature by extracting the public key from the certificate
56
50
  # embedded in the soap header and verifying the signature value with that.
57
51
  def signature_is_valid?
58
- node = soap.at_css('xmlns|SignedInfo', 'xmlns' => 'http://www.w3.org/2000/09/xmldsig#')
52
+ node = doc.at('xmlns|SignedInfo', xmlns: DSIG)
59
53
 
60
- node = node.canonicalize(
61
- mode = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0,
62
- inclusive_namespaces = nil, with_comments = false
63
- )
54
+ node = canonicalize_exclusively node
64
55
 
65
- signature = soap.at_css(
66
- 'xmlns|SignatureValue',
67
- 'xmlns' => 'http://www.w3.org/2000/09/xmldsig#'
68
- ).content
56
+ signature = doc.at('xmlns|SignatureValue', xmlns: DSIG).content
69
57
 
70
- signature = Base64.decode64(signature)
58
+ signature = decode(signature)
71
59
 
72
60
  certificate.public_key.verify(OpenSSL::Digest::SHA1.new, signature, node)
73
61
  end
74
62
 
75
- # Gets the application response from the response as an Nokogiri::XML
76
- # document
63
+ # Gets the application response from the response as an xml document
77
64
  def application_response
78
- ar = soap.at_css('mod|ApplicationResponse').content
79
- ar = Base64.decode64(ar)
80
- Nokogiri::XML(ar)
65
+ @application_response ||= extract_application_response(BXD)
81
66
  end
82
67
 
83
68
  def file_references
84
69
  return unless @command == :download_file_list
85
70
 
86
71
  @file_references ||= begin
87
- content = Nokogiri::XML @content
88
- descriptors = content.css('FileDescriptor')
72
+ xml = xml_doc content
73
+ descriptors = xml.css('FileDescriptor')
89
74
  descriptors.map { |descriptor| descriptor.at('FileReference').content }
90
75
  end
91
76
  end
92
77
 
78
+ def certificate
79
+ @certificate ||= begin
80
+ extract_cert(doc, 'BinarySecurityToken', OASIS_SECEXT)
81
+ end
82
+ end
83
+
84
+ def content
85
+ @content ||= begin
86
+ xml = xml_doc(application_response)
87
+
88
+ case @command
89
+ when :download_file
90
+ content_node = xml.at('xmlns|Content', xmlns: XML_DATA)
91
+ content_node.content if content_node
92
+ when :download_file_list
93
+ content_node = xml.remove_namespaces!.at('FileDescriptors')
94
+ content_node.to_xml if content_node
95
+ when :get_user_info
96
+ canonicalized_node(xml, XML_DATA, 'UserFileTypes')
97
+ when :upload_file
98
+ signature_node = xml.at('xmlns|Signature', xmlns: DSIG)
99
+ if signature_node
100
+ signature_node.remove
101
+ xml.canonicalize
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ def to_s
108
+ @soap
109
+ end
110
+
93
111
  private
94
112
 
95
113
  # Finds all reference nodes with digest values in the document and returns
96
114
  # a hash with uri as the key and digest as the value.
97
115
  def find_digest_values
98
116
  references = {}
99
- reference_nodes = soap.css(
100
- 'xmlns|Reference',
101
- 'xmlns' => 'http://www.w3.org/2000/09/xmldsig#'
102
- )
117
+ reference_nodes = doc.css('xmlns|Reference', xmlns: DSIG)
103
118
 
104
119
  reference_nodes.each do |node|
105
120
  uri = node.attr('URI')
106
- digest_value = node.at_css(
107
- 'xmlns|DigestValue',
108
- 'xmlns' => 'http://www.w3.org/2000/09/xmldsig#'
109
- ).content
121
+ digest_value = node.at('xmlns|DigestValue', xmlns: DSIG).content
110
122
 
111
123
  references[uri] = digest_value
112
124
  end
@@ -116,17 +128,12 @@ module Sepa
116
128
 
117
129
  # Finds nodes to verify by comparing their id's to the uris' in the
118
130
  # references hash.
119
- def find_nodes_to_verify(doc, references)
131
+ def find_nodes_to_verify(references)
120
132
  nodes = {}
121
133
 
122
- references.each do |uri, digest_value|
134
+ references.each do |uri, _digest_value|
123
135
  uri = uri.sub(/^#/, '')
124
- wsu = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd'
125
-
126
- node = doc.at_css(
127
- "[wsu|Id='#{uri}']",
128
- 'wsu' => wsu
129
- )
136
+ node = find_node_by_uri(uri)
130
137
 
131
138
  nodes[uri] = calculate_digest(node)
132
139
  end
@@ -135,45 +142,27 @@ module Sepa
135
142
  end
136
143
 
137
144
  def validate_document_format
138
- unless soap.respond_to?(:canonicalize)
139
- errors.add(:base, 'Document must be a Nokogiri XML file')
145
+ unless doc.respond_to?(:canonicalize)
146
+ errors.add(:base, 'Document must be a valid XML file')
140
147
  end
141
148
  end
142
149
 
143
150
  def document_must_validate_against_schema
144
- check_validity_against_schema(soap, 'soap.xsd')
151
+ check_validity_against_schema(doc, 'soap.xsd')
145
152
  end
146
153
 
147
- def extract_content
148
- xml = Nokogiri::XML(@application_response)
149
- xmlns = 'http://bxd.fi/xmldata/'
150
-
151
- case @command
152
- when :download_file
153
- content_node = xml.at('xmlns|Content', xmlns: xmlns)
154
- content_node.content if content_node
155
- when :download_file_list
156
- content_node = xml.remove_namespaces!.at('FileDescriptors')
157
- content_node.to_xml if content_node
158
- when :get_user_info
159
- canonicalized_node(xml, xmlns, 'UserFileTypes')
160
- when :upload_file
161
- signature_node = xml.at('xmlns|Signature', xmlns: 'http://www.w3.org/2000/09/xmldsig#')
162
- if signature_node
163
- signature_node.remove
164
- xml.canonicalize
165
- end
166
- end
154
+ def extract_application_response(namespace)
155
+ ar_node = doc.at('xmlns|ApplicationResponse', xmlns: namespace)
156
+ decode(ar_node.content)
167
157
  end
168
158
 
169
- def extract_application_response(namespace)
170
- if soap.respond_to? :at_css
171
- ar_node = soap.at_css('xmlns|ApplicationResponse', xmlns: namespace)
172
- end
159
+ def client_errors
160
+ client_error = error.to_s
161
+ errors.add(:base, client_error) unless client_error.empty?
162
+ end
173
163
 
174
- if ar_node
175
- Base64.decode64(ar_node.content)
176
- end
164
+ def find_node_by_uri(uri)
165
+ doc.at("[xmlns|Id='#{uri}']", xmlns: OASIS_UTILITY)
177
166
  end
178
167
 
179
168
  end
@@ -2,26 +2,27 @@ module Sepa
2
2
  class SoapBuilder
3
3
  include Utilities
4
4
 
5
- attr_reader :ar
5
+ attr_reader :application_request
6
6
 
7
7
  # SoapBuilder creates the SOAP structure.
8
8
  def initialize(params)
9
- @bank = params[:bank]
10
- @private_key = params[:private_key]
11
- @cert = params[:cert]
12
- @command = params[:command]
13
- @customer_id = params[:customer_id]
14
- @environment = params[:environment]
15
- @status = params[:status]
16
- @target_id = params[:target_id]
17
- @language = params[:language]
18
- @file_type = params[:file_type]
19
- @content = params[:content]
20
- @file_reference = params[:file_reference]
21
- @enc_cert = params[:enc_cert]
22
- @header_template = load_header_template
23
- @template = load_body_template SOAP_TEMPLATE_PATH
24
- @ar = ApplicationRequest.new(params)
9
+ @bank = params[:bank]
10
+ @cert = params[:cert]
11
+ @command = params[:command]
12
+ @content = params[:content]
13
+ @customer_id = params[:customer_id]
14
+ @enc_cert = params[:enc_cert]
15
+ @environment = params[:environment]
16
+ @file_reference = params[:file_reference]
17
+ @file_type = params[:file_type]
18
+ @language = params[:language]
19
+ @private_key = params[:private_key]
20
+ @status = params[:status]
21
+ @target_id = params[:target_id]
22
+
23
+ @application_request = ApplicationRequest.new params
24
+ @header_template = load_header_template
25
+ @template = load_body_template SOAP_TEMPLATE_PATH
25
26
 
26
27
  find_correct_bank_extension
27
28
  end
@@ -51,7 +52,7 @@ module Sepa
51
52
  inclusive_namespaces = nil, with_comments = false
52
53
  )
53
54
 
54
- Base64.encode64(sha1.digest(canon_node)).gsub(/\s+/, "")
55
+ encode(sha1.digest(canon_node)).gsub(/\s+/, "")
55
56
  end
56
57
 
57
58
  def calculate_signature(doc, node)
@@ -64,7 +65,7 @@ module Sepa
64
65
  )
65
66
 
66
67
  signature = @private_key.sign(sha1, canon_signed_info_node)
67
- Base64.encode64(signature).gsub(/\s+/, "")
68
+ encode(signature).gsub(/\s+/, "")
68
69
  end
69
70
 
70
71
  def load_header_template
@@ -4,12 +4,9 @@ module Sepa
4
4
  def calculate_digest(node)
5
5
  sha1 = OpenSSL::Digest::SHA1.new
6
6
 
7
- canon_node = node.canonicalize(
8
- mode = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0,
9
- inclusive_namespaces = nil, with_comments = false
10
- )
7
+ canon_node = canonicalize_exclusively node
11
8
 
12
- Base64.encode64(sha1.digest(canon_node)).gsub(/\s+/, "")
9
+ encode(sha1.digest(canon_node)).gsub(/\s+/, "")
13
10
  end
14
11
 
15
12
  # Takes a certificate, adds begin and end
@@ -17,9 +14,9 @@ module Sepa
17
14
  # can read it.
18
15
  def process_cert_value(cert_value)
19
16
  cert = "-----BEGIN CERTIFICATE-----\n"
20
- cert += cert_value.to_s.gsub(/\s+/, "").scan(/.{1,64}/).join("\n")
21
- cert += "\n"
22
- cert + "-----END CERTIFICATE-----"
17
+ cert << cert_value.to_s.gsub(/\s+/, "").scan(/.{1,64}/).join("\n")
18
+ cert << "\n"
19
+ cert << "-----END CERTIFICATE-----"
23
20
  end
24
21
 
25
22
  def format_cert(cert)
@@ -47,18 +44,14 @@ module Sepa
47
44
  # Extracts a certificate from a document and return it as an OpenSSL X509 certificate
48
45
  # Return nil is the node cannot be found
49
46
  def extract_cert(doc, node, namespace)
50
- return nil unless doc.respond_to? :at
51
-
52
47
  cert_raw = doc.at("xmlns|#{node}", 'xmlns' => namespace)
53
48
 
54
- return nil if cert_raw.nil?
55
-
56
49
  cert_raw = cert_raw.content.gsub(/\s+/, "")
57
50
 
58
51
  cert = process_cert_value(cert_raw)
59
52
 
60
53
  begin
61
- OpenSSL::X509::Certificate.new(cert)
54
+ x509_certificate(cert)
62
55
  rescue => e
63
56
  fail OpenSSL::X509::CertificateError,
64
57
  "The certificate could not be processed. It's most likely corrupted. OpenSSL had this to say: #{e}."
@@ -97,7 +90,7 @@ module Sepa
97
90
  fail ArgumentError
98
91
  end
99
92
 
100
- Nokogiri::XML(File.open(path))
93
+ xml_doc(File.open(path))
101
94
  end
102
95
 
103
96
  # Checks that the certificate in the application response is signed with the
@@ -116,7 +109,7 @@ module Sepa
116
109
  end
117
110
 
118
111
  def hmac(pin, csr)
119
- Base64.encode64(OpenSSL::HMAC.digest('sha1', pin, csr)).chop
112
+ encode(OpenSSL::HMAC.digest('sha1', pin, csr)).chop
120
113
  end
121
114
 
122
115
  def csr_to_binary(csr)
@@ -128,5 +121,27 @@ module Sepa
128
121
  content_node.canonicalize if content_node
129
122
  end
130
123
 
124
+ def xml_doc(value)
125
+ Nokogiri::XML value
126
+ end
127
+
128
+ def decode(value)
129
+ Base64.decode64 value
130
+ end
131
+
132
+ def canonicalize_exclusively(value)
133
+ value.canonicalize(mode = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0,
134
+ inclusive_namespaces = nil,
135
+ with_comments = false)
136
+ end
137
+
138
+ def x509_certificate(value)
139
+ OpenSSL::X509::Certificate.new value
140
+ end
141
+
142
+ def encode(value)
143
+ Base64.encode64 value
144
+ end
145
+
131
146
  end
132
147
  end