digidoc_client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []