digidoc_client 0.0.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.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in digidoc_client.gemspec
4
+ gemspec
data/README.markdown ADDED
@@ -0,0 +1,48 @@
1
+ Ruby client for Estonian DigiDoc service authentication and signing API.
2
+
3
+ ## Installation
4
+
5
+ Add gem dependency in your `Gemfile` and install the gem:
6
+
7
+ gem 'digidoc_client'
8
+
9
+ ## Usage
10
+
11
+ ### Authentication
12
+
13
+ client = Digidoc::Client.new
14
+ client.authenticate(
15
+ :phone => '+3725012345', :message_to_display => 'Authenticating',
16
+ :service_name => 'Testing'
17
+ )
18
+ client.authentication_status
19
+
20
+ ### Signing
21
+
22
+ client = Digidoc::Client.new
23
+ client.start_session
24
+ client.create_signed_doc
25
+ client.signed_doc_info
26
+
27
+ file1 = File.open('file1.pdf')
28
+ client.add_datafile(file1)
29
+ file2 = File.open('file2.pdf')
30
+ client.add_datafile(file2)
31
+
32
+ client.mobile_sign(:phone => '5012345', :role => ' My Company LLC / CTO')
33
+ client.sign_status
34
+
35
+ client.save_signed_doc do |content|
36
+ File.open('signed_document.ddoc', 'w') { |f| f.write(content) }
37
+ end
38
+
39
+ client.close_session
40
+
41
+ ## Digidoc specifications
42
+
43
+ [In English](http://www.sk.ee/upload/files/DigiDocService_spec_eng.pdf)
44
+ [In Estonian](http://www.sk.ee/upload/files/DigiDocService_spec_est.pdf)
45
+
46
+ ## Authors
47
+
48
+ [See this list](https://github.com/tarmotalu/digidoc_client/contributors)
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "digidoc/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "digidoc_client"
7
+ s.version = Digidoc::VERSION
8
+ s.authors = ["Tarmo Talu"]
9
+ s.email = ["tarmo.talu@gmail.com"]
10
+ s.homepage = "http://github.com/tarmotalu"
11
+ s.summary = %q{Ruby library to interact with Estonian DigiDoc services.}
12
+ s.description = %q{An easy way to interact with Estonian DigiDoc services.}
13
+
14
+ s.rubyforge_project = "digidoc_client"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ #s.add_development_dependency "rspec"
23
+ s.add_dependency 'httpclient', '>= 2.2.4'
24
+ s.add_dependency 'savon', '>= 0.9.7'
25
+ s.add_dependency 'mime-types', '>= 1.16'
26
+ s.add_dependency 'crack', '>= 0.1.8'
27
+ s.add_dependency 'nokogiri', '>= 1.4.0'
28
+ end
@@ -0,0 +1,343 @@
1
+ require 'ostruct'
2
+ require 'httpclient'
3
+ require 'savon'
4
+ require 'cgi'
5
+ require 'crack/xml'
6
+ require 'mime/types'
7
+ require 'digest/sha1'
8
+ require 'nokogiri'
9
+
10
+ module Digidoc
11
+ TargetNamespace = 'http://www.sk.ee/DigiDocService/DigiDocService_2_3.wsdl'
12
+ TestEndpointUrl = 'https://openxades.org:8443/DigiDocService'
13
+
14
+ class Client
15
+ attr_accessor :session_code, :endpoint_url, :respond_with_nested_struct, :embedded_datafiles
16
+
17
+ def initialize(endpoint_url = TestEndpointUrl)
18
+ self.endpoint_url = endpoint_url || TestEndpointUrl
19
+ self.respond_with_nested_struct = true
20
+ self.embedded_datafiles = []
21
+ end
22
+
23
+ # Authentication message
24
+ def authenticate(*args)
25
+ options = args.last || {}
26
+
27
+ phone = options.delete(:phone)
28
+ personal_code = options.delete(:personal_code)
29
+ country_code = options.delete(:country_code) || 'EE'
30
+ language = options.delete(:language) || 'EST'
31
+ service_name = options.delete(:service_name) || 'Testimine'
32
+ message_to_display = options.delete(:message_to_display) || 'Tuvastamine'
33
+ messaging_mode = options.delete(:messaging_mode) || 'asynchClientServer'
34
+ async_configuration = options.delete(:async_configuration) || 0
35
+ return_cert_data = options.key?(:return_cert_data) ? options.delete(:return_cer_data) : true
36
+ return_revocation_data = options.key?(:return_revocation_data) ? options.delete(:return_revocation_data) : true
37
+
38
+ # SP challenge token
39
+ sp_challenge = generate_sp_challenge
40
+ phone = ensure_area_code(phone)
41
+ self.session_code = nil
42
+
43
+ # Make webservice call
44
+ response = savon_client.request(:wsdl, 'MobileAuthenticate') do |soap|
45
+ soap.body = {'CountryCode' => country_code, 'PhoneNo' => phone, 'Language' => language, 'ServiceName' => service_name,
46
+ 'MessageToDisplay' => message_to_display, 'SPChallenge' => sp_challenge, 'MessagingMode' => messaging_mode,
47
+ 'AsyncConfiguration' => async_configuration, 'ReturnCertData' => return_cert_data,
48
+ 'ReturnRevocationData' => return_revocation_data, 'IdCode' => personal_code }
49
+ end
50
+
51
+ if soap_fault?(response)
52
+ result = response.to_hash[:fault]
53
+ else
54
+ result = response.to_hash[:mobile_authenticate_response]
55
+ self.session_code = result[:sesscode]
56
+ end
57
+ respond_with_hash_or_nested(result)
58
+ end
59
+
60
+ # Authentication status
61
+ def authentication_status(session_code = self.session_code)
62
+ response = savon_client.request(:wsdl, 'GetMobileAuthenticateStatus') do |soap|
63
+ soap.body = {'Sesscode' => session_code }
64
+ end
65
+
66
+ result = soap_fault?(response) ? response.to_hash[:fault] : response.to_hash[:get_mobile_authenticate_status_response]
67
+ respond_with_hash_or_nested(result)
68
+ end
69
+
70
+ # Starts and holds session
71
+ def start_session(*args)
72
+ self.session_code = nil
73
+ self.embedded_datafiles = []
74
+ options = args.last || {}
75
+ signed_doc_file = options.delete(:signed_doc_file)
76
+ signed_doc_xml = signed_doc_file.read if signed_doc_file
77
+
78
+ response = savon_client.request(:wsdl, 'StartSession') do |soap|
79
+ soap.body = { 'bHoldSession' => true, 'SigDocXML' => signed_doc_xml}
80
+ end
81
+
82
+ if soap_fault?(response)
83
+ result = response.to_hash[:fault]
84
+ else
85
+ result = response.to_hash[:start_session_response]
86
+ self.session_code = result[:sesscode]
87
+ end
88
+ respond_with_hash_or_nested(result)
89
+ end
90
+
91
+ # Creates DigiDoc container
92
+ def create_signed_doc(*args)
93
+ options = args.last || {}
94
+
95
+ session_code = options.delete(:session_code) || self.session_code
96
+ version = options.delete(:version) || '1.3'
97
+
98
+ response = savon_client.request(:wsdl, 'CreateSignedDoc') do |soap|
99
+ soap.body = {'Sesscode' => session_code, 'Format' => 'DIGIDOC-XML', 'Version' => version}
100
+ end
101
+
102
+ result = soap_fault?(response) ? response.to_hash[:fault] : response.to_hash[:create_signed_doc_response]
103
+ respond_with_hash_or_nested(result)
104
+ end
105
+
106
+ def prepare_signature(*args)
107
+ options = args.last || {}
108
+
109
+ session_code = options.delete(:session_code) || self.session_code
110
+ signers_certificate = options.delete(:signers_certificate)
111
+ signers_token_id = options.delete(:signers_token_id)
112
+ signing_profile = options.delete(:signing_profile)
113
+ country_name = options.delete(:country_name) || 'Eesti'
114
+ state_or_province = options.delete(:state_or_province)
115
+ role = options.delete(:role)
116
+ city = options.delete(:city)
117
+ postal_code = options.delete(:postal_code)
118
+
119
+ response = savon_client.request(:wsdl, 'PrepareSignature') do |soap|
120
+ soap.body = {'Sesscode' => session_code, 'SignersCertificate' => signers_certificate,
121
+ 'SignersTokenId' => signers_token_id, 'Role' => role, 'City' => city,
122
+ 'State' => state_or_province, 'PostalCode' => postal_code, 'Country' => country_name, 'SigningProfile' => signing_profile }
123
+ end
124
+
125
+ result = soap_fault?(response) ? response.to_hash[:fault] : response.to_hash[:prepare_signature_response]
126
+ respond_with_hash_or_nested(result)
127
+ end
128
+
129
+ def finalize_signature(*args)
130
+ options = args.last || {}
131
+
132
+ session_code = options.delete(:session_code) || self.session_code
133
+ signature = options.delete(:signature)
134
+ signature_id = options.delete(:signature_id)
135
+
136
+ response = savon_client.request(:wsdl, 'FinalizeSignature') do |soap|
137
+ soap.body = {'Sesscode' => session_code, 'SignatureValue' => signature, 'SignatureId' => signature_id}
138
+ end
139
+
140
+ result = soap_fault?(response) ? response.to_hash[:fault] : response.to_hash[:finalize_signature_response]
141
+ respond_with_hash_or_nested(result)
142
+ end
143
+
144
+ def notary(*args)
145
+ options = args.last || {}
146
+
147
+ session_code = options.delete(:session_code) || self.session_code
148
+ signature_id = options.delete(:signature_id)
149
+
150
+ response = savon_client.request(:wsdl, 'GetNotary') do |soap|
151
+ soap.body = {'Sesscode' => session_code, 'SignatureId' => signature_id}
152
+ end
153
+
154
+ result = soap_fault?(response) ? response.to_hash[:fault] : response.to_hash[:get_notary_response]
155
+ respond_with_hash_or_nested(result)
156
+ end
157
+
158
+ # Sign DigiDoc container
159
+ def mobile_sign(*args)
160
+ options = args.last || {}
161
+
162
+ session_code = options.delete(:session_code) || self.session_code
163
+ phone = options.delete(:phone)
164
+ personal_code = options.delete(:personal_code)
165
+ country_code = options.delete(:country_code) || 'EE'
166
+ country_name = options.delete(:country_name) || 'Eesti'
167
+ language = options.delete(:language) || 'EST'
168
+ service_name = options.delete(:service_name) || 'Testimine'
169
+ message_to_display = options.delete(:message_to_display) || 'Allkirjastamine'
170
+ messaging_mode = options.delete(:messaging_mode) || 'asynchClientServer'
171
+ async_configuration = options.delete(:async_configuration) || 0
172
+ return_doc_info = options.key?(:return_doc_info) ? options.delete(:return_doc_info) : true
173
+ return_doc_data = options.key?(:return_doc_data) ? options.delete(:return_doc_data) : true
174
+ state_or_province = options.delete(:state_or_province)
175
+ role = options.delete(:role)
176
+ city = options.delete(:city)
177
+ postal_code = options.delete(:postal_code)
178
+ phone = ensure_area_code(phone)
179
+
180
+ response = savon_client.request(:wsdl, 'MobileSign') do |soap|
181
+ soap.body = {'Sesscode' => session_code, 'SignersCountry' => country_code, 'CountryName' => country_name,
182
+ 'SignerPhoneNo' => phone, 'Language' => language, 'ServiceName' => service_name,
183
+ 'AdditionalDataToBeDisplayed' => message_to_display, 'MessagingMode' => messaging_mode,
184
+ 'AsyncConfiguration' => async_configuration, 'ReturnDocInfo' => return_doc_info,
185
+ 'ReturnDocData' => return_doc_data, 'SignerIDCode' => personal_code, 'Role' => role, 'City' => city,
186
+ 'StateOrProvince' => state_or_province, 'PostalCode' => postal_code }
187
+ end
188
+
189
+ result = soap_fault?(response) ? response.to_hash[:fault] : response.to_hash[:mobile_sign_response]
190
+ respond_with_hash_or_nested(result)
191
+ end
192
+
193
+ # Get session status info.
194
+ def sign_status(*args)
195
+ options = args.last || {}
196
+
197
+ session_code = options.delete(:session_code) || self.session_code
198
+ return_doc_info = options.key?(:return_doc_info) ? options.delete(:return_doc_info) : false
199
+ wait_signature = options.key?(:wait_signature) ? options.delete(:wait_signature) : false
200
+
201
+ response = savon_client.request(:wsdl, 'GetStatusInfo') do |soap|
202
+ soap.body = {'Sesscode' => session_code, 'ReturnDocInfo' => return_doc_info, 'WaitSignature' => wait_signature}
203
+ end
204
+
205
+ result = soap_fault?(response) ? response.to_hash[:fault] : response.to_hash[:get_status_info_response]
206
+ respond_with_hash_or_nested(result)
207
+ end
208
+
209
+ # Get DigiDoc container status
210
+ def signed_doc_info(*args)
211
+ options = args.last || {}
212
+ session_code = options.delete(:session_code) || self.session_code
213
+
214
+ response = savon_client.request(:wsdl, 'GetSignedDocInfo') do |soap|
215
+ soap.body = {'Sesscode' => session_code }
216
+ end
217
+ result = soap_fault?(response) ? response.to_hash[:fault] : response.to_hash[:get_signed_doc_info_response]
218
+ respond_with_hash_or_nested(result)
219
+ end
220
+
221
+ # Get DigiDoc container
222
+ def save_signed_doc(*args, &block)
223
+ options = args.last || {}
224
+ session_code = options.delete(:session_code) || self.session_code
225
+
226
+ response = savon_client.request(:wsdl, 'GetSignedDoc') do |soap|
227
+ soap.body = {'Sesscode' => session_code }
228
+ end
229
+
230
+ if soap_fault?(response)
231
+ result = respond_with_hash_or_nested(response.to_hash[:fault])
232
+ else
233
+ escaped = Crack::XML.parse(response.http.body).to_hash['SOAP_ENV:Envelope']['SOAP_ENV:Body']['d:GetSignedDocResponse']['SignedDocData']
234
+ # TODO: is escaping needed? - it removes original escaped & form XML
235
+ digidoc_container = escaped#CGI.unescapeHTML(escaped)
236
+
237
+ if embedded_datafiles.present?
238
+ xmldata = Nokogiri::XML(digidoc_container)
239
+ xmldata.root.elements.each { |el| el.replace(embedded_datafiles.shift) if el.name == 'DataFile' }
240
+ digidoc_container = xmldata.to_xml
241
+ end
242
+
243
+ if block_given?
244
+ yield digidoc_container
245
+ else
246
+ digidoc_container
247
+ end
248
+ end
249
+ end
250
+
251
+ # Closes current session
252
+ def close_session(session_code = self.session_code)
253
+ response = savon_client.request(:wsdl, 'CloseSession') do |soap|
254
+ soap.body = {'Sesscode' => session_code }
255
+ end
256
+ self.session_code = nil
257
+
258
+ result = soap_fault?(response) ? response.to_hash[:fault] : response.to_hash[:close_session_response]
259
+ respond_with_hash_or_nested(result)
260
+ end
261
+
262
+ # Add datafile to DigiDoc container
263
+ def add_datafile(file, *args)
264
+ options = args.last || {}
265
+
266
+ session_code = options.delete(:session_code) || self.session_code
267
+ filename = options.delete(:filename) || File.basename(file.path)
268
+ mime_type = options[:mime_type] || calc_mime_type(file)
269
+ use_hashcode = false #options.key?(:use_hashcode) || true
270
+ filename = filename.gsub('/', '-')
271
+
272
+ response = savon_client.request(:wsdl, 'AddDataFile') do |soap|
273
+ file_content = Base64.encode64(file.read)
274
+ # Upload file to webservice
275
+ if use_hashcode
276
+ # Calculate sha1 from file
277
+ datafile = datafile(filename, mime_type, file.size, file_content, embedded_datafiles.size)
278
+ self.embedded_datafiles << datafile
279
+ hex_sha1 = Digest::SHA1.hexdigest(datafile)
280
+ digest_value = Base64.encode64(hex_sha1.lines.to_a.pack('H*'))
281
+ soap.body = {'Sesscode' => session_code, 'FileName' => filename, 'MimeType' => mime_type, 'ContentType' => 'HASHCODE',
282
+ 'Size' => file.size, 'DigestType' => 'sha1', 'DigestValue' => digest_value}
283
+ else
284
+ soap.body = {'Sesscode' => session_code, 'FileName' => filename, 'MimeType' => mime_type, 'ContentType' => 'EMBEDDED_BASE64',
285
+ 'Size' => file.size, 'Content' => file_content}
286
+ end
287
+ end
288
+
289
+ result = soap_fault?(response) ? response.to_hash[:fault] : response.to_hash[:add_data_file_response]
290
+ respond_with_hash_or_nested(result)
291
+ end
292
+
293
+ private
294
+
295
+ def soap_fault?(response)
296
+ response.http.body =~ /<*Fault>/
297
+ end
298
+
299
+ def ensure_area_code(phone)
300
+ phone =~ /^\+/ ? phone : "+372#{phone}" unless phone.blank?
301
+ end
302
+
303
+ def savon_client
304
+ Savon::Client.new do |wsdl, http|
305
+ wsdl.endpoint = self.endpoint_url
306
+ wsdl.namespace = TargetNamespace
307
+ http.open_timeout = 10
308
+ http.auth.ssl.verify_mode = :none # todo: add env dependency
309
+ end
310
+ end
311
+
312
+ def datafile(filename, mime_type, size, content, id)
313
+ datafile = "<DataFile ContentType=\"EMBEDDED_BASE64\" Filename=\"#{filename}\" Id=\"D#{id}\" MimeType=\"#{mime_type}\" Size=\"#{size}\">#{content}</DataFile>"
314
+ end
315
+
316
+ def calc_mime_type(file)
317
+ return unless file
318
+ MIME::Types.type_for(File.basename(file.path)).first.try(:content_type) || 'text/plain'
319
+ end
320
+
321
+ def respond_with_hash_or_nested(hash)
322
+ if respond_with_nested_struct
323
+ NestedOpenStruct.new(hash)
324
+ else
325
+ hash
326
+ end
327
+ end
328
+
329
+ # Hex ID generator
330
+ def generate_unique_hex(codeLength)
331
+ validChars = ("A".."F").to_a + ("0".."9").to_a
332
+ length = validChars.size
333
+ hexCode = ''
334
+ 1.upto(codeLength) { |i| hexCode << validChars[rand(length-1)] }
335
+ hexCode
336
+ end
337
+
338
+ # Generates unique challenge code (consumer token that gets scrumbled by gateway)
339
+ def generate_sp_challenge
340
+ generate_unique_hex(20)
341
+ end
342
+ end
343
+ end
@@ -0,0 +1,3 @@
1
+ module Digidoc
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,2 @@
1
+ require "digidoc/version"
2
+ require "digidoc/client"
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: digidoc_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tarmo Talu
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: httpclient
16
+ requirement: &70237639073840 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 2.2.4
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70237639073840
25
+ - !ruby/object:Gem::Dependency
26
+ name: savon
27
+ requirement: &70237639072700 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 0.9.7
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70237639072700
36
+ - !ruby/object:Gem::Dependency
37
+ name: mime-types
38
+ requirement: &70237639071760 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '1.16'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70237639071760
47
+ - !ruby/object:Gem::Dependency
48
+ name: crack
49
+ requirement: &70237639070580 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: 0.1.8
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70237639070580
58
+ - !ruby/object:Gem::Dependency
59
+ name: nokogiri
60
+ requirement: &70237639069620 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: 1.4.0
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *70237639069620
69
+ description: An easy way to interact with Estonian DigiDoc services.
70
+ email:
71
+ - tarmo.talu@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - .gitignore
77
+ - Gemfile
78
+ - README.markdown
79
+ - Rakefile
80
+ - digidoc_client.gemspec
81
+ - lib/digidoc/client.rb
82
+ - lib/digidoc/version.rb
83
+ - lib/digidoc_client.rb
84
+ homepage: http://github.com/tarmotalu
85
+ licenses: []
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubyforge_project: digidoc_client
104
+ rubygems_version: 1.8.11
105
+ signing_key:
106
+ specification_version: 3
107
+ summary: Ruby library to interact with Estonian DigiDoc services.
108
+ test_files: []