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