sepafm 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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