ecertic 0.1.0
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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +46 -0
- data/.travis.yml +9 -0
- data/CHANGELOG +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/Guardfile +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +74 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/ecertic.gemspec +44 -0
- data/lib/ecertic.rb +71 -0
- data/lib/ecertic/api.rb +23 -0
- data/lib/ecertic/api/documents_service.rb +19 -0
- data/lib/ecertic/api/otps_service.rb +20 -0
- data/lib/ecertic/api/service.rb +13 -0
- data/lib/ecertic/api/tokens_service.rb +19 -0
- data/lib/ecertic/callback.rb +14 -0
- data/lib/ecertic/client.rb +202 -0
- data/lib/ecertic/default.rb +34 -0
- data/lib/ecertic/errors.rb +45 -0
- data/lib/ecertic/resources/base.rb +14 -0
- data/lib/ecertic/resources/document.rb +8 -0
- data/lib/ecertic/resources/otp.rb +13 -0
- data/lib/ecertic/resources/otp/request.rb +21 -0
- data/lib/ecertic/resources/otp/status.rb +11 -0
- data/lib/ecertic/resources/statusable.rb +22 -0
- data/lib/ecertic/resources/token/instance.rb +13 -0
- data/lib/ecertic/resources/token/validation.rb +13 -0
- data/lib/ecertic/response.rb +37 -0
- data/lib/ecertic/utils.rb +93 -0
- data/lib/ecertic/version.rb +5 -0
- metadata +211 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ecertic
|
4
|
+
module API
|
5
|
+
class DocumentsService < Service
|
6
|
+
def pdf(document_id, options = {})
|
7
|
+
client.get("/pdf/#{document_id}/0", options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def html(document_id, options = {})
|
11
|
+
client.get("/html/#{document_id}/0", options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def signed(document_id, options = {})
|
15
|
+
client.get("/signed/#{document_id}/0", options)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ecertic
|
4
|
+
module API
|
5
|
+
class OTPsService < Service
|
6
|
+
def create(attributes, options = {})
|
7
|
+
Ecertic::Utils.validate_mandatory_attributes(attributes, [:movil, :pdf_files])
|
8
|
+
attributes[:pdf_files] = Utils.encode_files(attributes[:pdf_files])
|
9
|
+
response = client.post("/sms", attributes, options)
|
10
|
+
Resource::OTP::Request.new(response.body)
|
11
|
+
end
|
12
|
+
|
13
|
+
def status(token, options = {})
|
14
|
+
attributes = { token: token }
|
15
|
+
response = client.post("/status", attributes, options)
|
16
|
+
Resource::OTP::Status.new(response.body)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ecertic
|
4
|
+
module API
|
5
|
+
class TokensService < Service
|
6
|
+
def retrieve(token, options = {})
|
7
|
+
attributes = { token: token }
|
8
|
+
response = client.post("/token", attributes, options)
|
9
|
+
Resource::Token::Instance.new(response.body)
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate(token, otp, options = {})
|
13
|
+
attributes = { token: token, otp: otp }
|
14
|
+
response = client.post("/validate", attributes, options)
|
15
|
+
Resource::Token::Validation.new(response.body)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ecertic
|
4
|
+
class Client
|
5
|
+
include Ecertic::API
|
6
|
+
attr_accessor :apikey, :secret, :user_agent, :connection
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
defaults = Ecertic::Default.options
|
10
|
+
Ecertic::Default.keys.each do |key|
|
11
|
+
instance_variable_set(:"@#{key}", options[key] || defaults[key])
|
12
|
+
end
|
13
|
+
@connection = connection || self.class.default_connection
|
14
|
+
@services = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.default_connection
|
18
|
+
Thread.current[:ecertic_client_default_connection] ||= begin
|
19
|
+
Faraday.new do |builder|
|
20
|
+
builder.use Faraday::Response::RaiseError
|
21
|
+
builder.response :json,
|
22
|
+
content_type: /\bjson$/,
|
23
|
+
preserve_raw: true, parser_options: { symbolize_names: true }
|
24
|
+
builder.adapter :net_http_persistent
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def base_url
|
30
|
+
@base_url.chomp("/")
|
31
|
+
end
|
32
|
+
|
33
|
+
def get(path, options = {})
|
34
|
+
execute :get, path, nil, options.to_h
|
35
|
+
end
|
36
|
+
|
37
|
+
def post(path, data = nil, options = {})
|
38
|
+
execute :post, path, data, options
|
39
|
+
end
|
40
|
+
|
41
|
+
def execute(method, path, data = nil, options = {})
|
42
|
+
request(method, path, data, options)
|
43
|
+
end
|
44
|
+
|
45
|
+
def request(method, path, data = nil, options = {})
|
46
|
+
request_options = request_options(method, options, data)
|
47
|
+
uri = "#{base_url}#{path}"
|
48
|
+
|
49
|
+
begin
|
50
|
+
request_start = Time.now
|
51
|
+
log_request(method, path, request_options[:body], request_options[:headers])
|
52
|
+
response = connection.run_request(method, uri, request_options[:body], request_options[:headers]) do |req|
|
53
|
+
# req.options.open_timeout = Ecertic.open_timeout
|
54
|
+
# req.options.timeout = Ecertic.read_timeout
|
55
|
+
end
|
56
|
+
log_response(request_start, method, path, response.status, response.body)
|
57
|
+
response
|
58
|
+
rescue StandardError => e
|
59
|
+
log_response_error(request_start, e, method, path)
|
60
|
+
case e
|
61
|
+
when Faraday::ClientError
|
62
|
+
if e.response
|
63
|
+
handle_error_response(e.response)
|
64
|
+
else
|
65
|
+
handle_network_error(e)
|
66
|
+
end
|
67
|
+
else
|
68
|
+
raise
|
69
|
+
end
|
70
|
+
end
|
71
|
+
Ecertic::Response.from_faraday_response(response)
|
72
|
+
end
|
73
|
+
|
74
|
+
private def handle_network_error(error)
|
75
|
+
Ecertic::Utils.log_error("Ecertic network error", error_message: error.message)
|
76
|
+
|
77
|
+
message = case error
|
78
|
+
when Faraday::ConnectionFailed
|
79
|
+
"Unexpected error communicating when trying to connect to " \
|
80
|
+
"Ecertic. You may be seeing this message because your DNS is not " \
|
81
|
+
"working. To check, try running `host http://api.otpsecure.net` from the " \
|
82
|
+
"command line."
|
83
|
+
|
84
|
+
when Faraday::SSLError
|
85
|
+
"Could not establish a secure connection to Ecertic, you " \
|
86
|
+
"may need to upgrade your OpenSSL version. To check, try running " \
|
87
|
+
"`openssl s_client -connect api.otpsecure.net:443` from the command " \
|
88
|
+
"line."
|
89
|
+
|
90
|
+
when Faraday::TimeoutError
|
91
|
+
"Could not connect to Ecertic (#{ Ecertic.api_base}). " \
|
92
|
+
"Please check your internet connection and try again."
|
93
|
+
|
94
|
+
else
|
95
|
+
"Unexpected error communicating with Ecertic."
|
96
|
+
end
|
97
|
+
raise APIConnectionError, message + "\n\n(Network error: #{error.message})"
|
98
|
+
end
|
99
|
+
|
100
|
+
private def handle_error_response(http_response)
|
101
|
+
begin
|
102
|
+
response = Ecertic::Response.new(http_response)
|
103
|
+
rescue JSON::ParserError
|
104
|
+
raise general_api_error(http_response[:status], http_response[:body])
|
105
|
+
end
|
106
|
+
|
107
|
+
raise specific_api_error(response)
|
108
|
+
end
|
109
|
+
|
110
|
+
private def general_api_error(status, body)
|
111
|
+
APIError.new("Invalid response object from API: #{body.inspect} " \
|
112
|
+
"(HTTP response code was #{status})",
|
113
|
+
http_status: status, http_body: body)
|
114
|
+
end
|
115
|
+
|
116
|
+
private def specific_api_error(response)
|
117
|
+
Ecertic::Utils.log_error("Ecertic API error", status: response.status)
|
118
|
+
|
119
|
+
error = case response.status
|
120
|
+
when 400, 404
|
121
|
+
InvalidRequestError
|
122
|
+
when 401
|
123
|
+
AuthenticationError
|
124
|
+
else
|
125
|
+
APIError
|
126
|
+
end
|
127
|
+
error.new(response.body, response: response)
|
128
|
+
end
|
129
|
+
|
130
|
+
private def base_options
|
131
|
+
{
|
132
|
+
headers: {
|
133
|
+
"Accept" => "application/json",
|
134
|
+
"User-Agent" => format_user_agent,
|
135
|
+
},
|
136
|
+
}
|
137
|
+
end
|
138
|
+
|
139
|
+
private def request_options(method, options = {}, data = nil)
|
140
|
+
base_options.tap do |options|
|
141
|
+
add_body!(options, data) if data
|
142
|
+
add_auth_options!(method, options, data) if data
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
private def add_body!(options, data)
|
147
|
+
options[:headers]["Content-Type"] = content_type(options[:headers])
|
148
|
+
options[:body] = content_data(options[:headers], data)
|
149
|
+
end
|
150
|
+
|
151
|
+
private def add_auth_options!(method, options, data)
|
152
|
+
timestamp = (Time.now.to_f * 1000).to_i.to_s
|
153
|
+
hmac = hmac(method, timestamp, options[:body], options[:headers]["Content-Type"])
|
154
|
+
options[:headers]["Date"] = timestamp
|
155
|
+
options[:headers]["Authorization"] = "Hmac #{apikey}:#{hmac}"
|
156
|
+
end
|
157
|
+
|
158
|
+
private def hmac(method, timestamp, data, content_type)
|
159
|
+
md5 = Digest::MD5.hexdigest(data)
|
160
|
+
signature = "#{method.upcase}\n#{md5}\n#{content_type}\n#{timestamp}"
|
161
|
+
OpenSSL::HMAC.hexdigest("SHA1", secret, signature)
|
162
|
+
end
|
163
|
+
|
164
|
+
private def format_user_agent
|
165
|
+
if user_agent.to_s.empty?
|
166
|
+
Ecertic::Default::USER_AGENT
|
167
|
+
else
|
168
|
+
"#{Ecertic::Default::USER_AGENT} #{user_agent}"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
private def content_type(headers)
|
173
|
+
headers["Content-Type"] || "application/json"
|
174
|
+
end
|
175
|
+
|
176
|
+
private def content_data(headers, data)
|
177
|
+
headers["Content-Type"] == "application/json" ? data.to_json : data
|
178
|
+
end
|
179
|
+
|
180
|
+
private def log_request(method, path, body, headers)
|
181
|
+
Ecertic::Utils.log_info("Request to Ecertic API", method: method, path: path)
|
182
|
+
Ecertic::Utils.log_debug("Request details", body: body, headers: headers)
|
183
|
+
end
|
184
|
+
|
185
|
+
private def log_response(request_start, method, path, status, body)
|
186
|
+
Ecertic::Utils.log_info("Response from Ecertic API",
|
187
|
+
elapsed: Time.now - request_start,
|
188
|
+
method: method,
|
189
|
+
path: path,
|
190
|
+
status: status)
|
191
|
+
Ecertic::Utils.log_debug("Response details", body: body)
|
192
|
+
end
|
193
|
+
|
194
|
+
private def log_response_error(request_start, error, method, path)
|
195
|
+
Ecertic::Utils.log_error("Request error",
|
196
|
+
elapsed: Time.now - request_start,
|
197
|
+
error_message: error.message,
|
198
|
+
method: method,
|
199
|
+
path: path)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ecertic
|
4
|
+
module Default
|
5
|
+
BASE_URL = "https://api.otpsecure.net"
|
6
|
+
USER_AGENT = "ecertic-ruby/#{VERSION}"
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def keys
|
10
|
+
@keys ||= [ :base_url, :apikey, :secret, :user_agent ]
|
11
|
+
end
|
12
|
+
|
13
|
+
def options
|
14
|
+
Hash[keys.map { |key| [key, send(key)] }]
|
15
|
+
end
|
16
|
+
|
17
|
+
def base_url
|
18
|
+
ENV["ECERTIC_BASE_URL"] || BASE_URL
|
19
|
+
end
|
20
|
+
|
21
|
+
def apikey
|
22
|
+
ENV["ECERTIC_APIKEY"]
|
23
|
+
end
|
24
|
+
|
25
|
+
def secret
|
26
|
+
ENV["ECERTIC_SECRET"]
|
27
|
+
end
|
28
|
+
|
29
|
+
def user_agent
|
30
|
+
ENV["ECERTIC_USER_AGENT"]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ecertic
|
4
|
+
# Error is the base error from which all other more specific Ecertic errors derive.
|
5
|
+
class Error < StandardError
|
6
|
+
attr_reader :message
|
7
|
+
attr_reader :http_status
|
8
|
+
attr_reader :http_headers
|
9
|
+
attr_reader :http_body
|
10
|
+
attr_reader :response
|
11
|
+
|
12
|
+
def initialize(message = nil, http_status: nil, http_headers: nil, http_body: nil, response: nil)
|
13
|
+
@message = message
|
14
|
+
@http_status = http_status || response&.status
|
15
|
+
@http_headers = http_headers || response&.headers
|
16
|
+
@http_body = http_body || response&.body
|
17
|
+
@response = response
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
status_string = @http_status.nil? ? "" : "(Status #{@http_status}) "
|
22
|
+
"#{status_string}#{@message}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# InvalidRequestError is raised when a request is initiated with invalid parameters.
|
27
|
+
class InvalidRequestError < Error
|
28
|
+
end
|
29
|
+
|
30
|
+
# AuthenticationError is raised when invalid credentials are used to connect Ecertic's servers.
|
31
|
+
class AuthenticationError < Error
|
32
|
+
end
|
33
|
+
|
34
|
+
# APIConnectionError is raised in the event that the SDK can't connect to Ecertic's servers.
|
35
|
+
class APIConnectionError < Error
|
36
|
+
end
|
37
|
+
|
38
|
+
# SignatureVerificationError is raised when the decryption for a callback fails
|
39
|
+
class SignatureVerificationError < Error
|
40
|
+
end
|
41
|
+
|
42
|
+
# APIError is a generic error that may be raised in cases where none of the ther named errors cover the problem.
|
43
|
+
class APIError < Error
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ecertic
|
4
|
+
module Resource
|
5
|
+
module OTP
|
6
|
+
class Request < Base
|
7
|
+
attr_accessor :id
|
8
|
+
attr_accessor :customid
|
9
|
+
attr_accessor :clientOtp
|
10
|
+
attr_accessor :clientToken
|
11
|
+
attr_accessor :pdf_shortUrl
|
12
|
+
attr_accessor :html_shortUrl
|
13
|
+
alias uuid id
|
14
|
+
alias otp clientOtp
|
15
|
+
alias token clientToken
|
16
|
+
alias pdf_url pdf_shortUrl
|
17
|
+
alias html_url html_shortUrl
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|