domainnameapi 1.0.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/CHANGELOG.md +47 -0
- data/LICENSE +201 -0
- data/README.md +995 -0
- data/lib/domainnameapi/client.rb +294 -0
- data/lib/domainnameapi/php.rb +322 -0
- data/lib/domainnameapi/rest.rb +1747 -0
- data/lib/domainnameapi/telemetry.rb +472 -0
- data/lib/domainnameapi/transport.rb +89 -0
- data/lib/domainnameapi/version.rb +6 -0
- data/lib/domainnameapi.rb +16 -0
- metadata +56 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'openssl'
|
|
6
|
+
require 'securerandom'
|
|
7
|
+
require 'uri'
|
|
8
|
+
|
|
9
|
+
module DomainNameApi
|
|
10
|
+
# Error and performance reporting to Sentry, ported from php-dna's
|
|
11
|
+
# SharedApiConfigAndUtilsTrait. Mixed into Rest; it reads the client's
|
|
12
|
+
# last request/response state. Telemetry never raises into the caller and
|
|
13
|
+
# never blocks it: events are posted from a background thread.
|
|
14
|
+
module Telemetry
|
|
15
|
+
# The Sentry DSN for ruby-dna; filled in when the Sentry project exists.
|
|
16
|
+
DEFAULT_SENTRY_DSN = ''
|
|
17
|
+
|
|
18
|
+
APPLICATION = 'DomainNameAPI-Ruby'
|
|
19
|
+
|
|
20
|
+
# Successful calls sampled for performance: 40 out of 1000 (4%).
|
|
21
|
+
PERFORMANCE_SAMPLE_RATE = 40
|
|
22
|
+
|
|
23
|
+
# Expected, high-volume errors that are never reported.
|
|
24
|
+
DEFAULT_IGNORED_ERRORS = [
|
|
25
|
+
'Domain not found',
|
|
26
|
+
'ERR_DOMAIN_NOT_FOUND',
|
|
27
|
+
'Reseller not found',
|
|
28
|
+
'Domain is not in updateable status',
|
|
29
|
+
'balance is not sufficient',
|
|
30
|
+
'Price definition not found',
|
|
31
|
+
'TLD is not supported',
|
|
32
|
+
'Invalid API credentials',
|
|
33
|
+
'could not be found',
|
|
34
|
+
'Insufficent reseller balance',
|
|
35
|
+
'Redirect information not found'
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
IP_CACHE_TTL = 512
|
|
39
|
+
MASK_KEY_RE = /auth|epp|password|passwd|secret|token|api[_-]?key/i.freeze
|
|
40
|
+
|
|
41
|
+
# Default sender: POST in a background thread with short timeouts.
|
|
42
|
+
# A replacement is any callable taking (url, payload_hash, auth_header,
|
|
43
|
+
# resolve_ip) where resolve_ip says the payload still needs the server's
|
|
44
|
+
# public IP in tags.ip_address.
|
|
45
|
+
THREAD_SENDER = lambda do |url, payload, auth_header, resolve_ip|
|
|
46
|
+
Thread.new do
|
|
47
|
+
begin
|
|
48
|
+
payload['tags']['ip_address'] = Telemetry.server_ip if resolve_ip
|
|
49
|
+
Telemetry.post(url, Telemetry.encode(payload), auth_header)
|
|
50
|
+
rescue StandardError, ScriptError
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@ip_cache = nil
|
|
58
|
+
@ip_cache_at = 0.0
|
|
59
|
+
@ip_mutex = Mutex.new
|
|
60
|
+
|
|
61
|
+
class << self
|
|
62
|
+
# The server's public IP (api.ipify.org), cached for IP_CACHE_TTL seconds.
|
|
63
|
+
def server_ip
|
|
64
|
+
@ip_mutex.synchronize do
|
|
65
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
66
|
+
return @ip_cache if @ip_cache && (now - @ip_cache_at) < IP_CACHE_TTL
|
|
67
|
+
|
|
68
|
+
ip = fetch_ip
|
|
69
|
+
if ip
|
|
70
|
+
@ip_cache = ip
|
|
71
|
+
@ip_cache_at = now
|
|
72
|
+
return ip
|
|
73
|
+
end
|
|
74
|
+
'unknown'
|
|
75
|
+
end
|
|
76
|
+
rescue StandardError
|
|
77
|
+
'unknown'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def fetch_ip
|
|
81
|
+
uri = URI('https://api.ipify.org')
|
|
82
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
83
|
+
http.use_ssl = true
|
|
84
|
+
http.open_timeout = 1
|
|
85
|
+
http.read_timeout = 2
|
|
86
|
+
body = http.get(uri.request_uri).body.to_s.strip
|
|
87
|
+
body =~ /\A[0-9a-fA-F:.]+\z/ ? body : nil
|
|
88
|
+
rescue StandardError
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def post(url, body, auth_header)
|
|
93
|
+
return if body.nil? || body.empty?
|
|
94
|
+
|
|
95
|
+
uri = URI(url)
|
|
96
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
97
|
+
http.use_ssl = uri.scheme == 'https'
|
|
98
|
+
http.open_timeout = 0.2
|
|
99
|
+
http.read_timeout = 0.5
|
|
100
|
+
req = Net::HTTP::Post.new(uri.request_uri)
|
|
101
|
+
req['Content-Type'] = 'application/json'
|
|
102
|
+
req['X-Sentry-Auth'] = auth_header.sub(/\AX-Sentry-Auth:\s*/, '')
|
|
103
|
+
req.body = body
|
|
104
|
+
http.request(req)
|
|
105
|
+
rescue StandardError
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# JSON with invalid UTF-8 replaced, so a binary body never drops an event.
|
|
110
|
+
def encode(payload)
|
|
111
|
+
Php.json_encode(Php.scrub_utf8(payload))
|
|
112
|
+
rescue StandardError
|
|
113
|
+
''
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Parse https://public[:secret]@host/project into its parts, or nil.
|
|
117
|
+
def parse_dsn(dsn)
|
|
118
|
+
return nil if dsn.nil? || dsn.to_s.strip.empty?
|
|
119
|
+
|
|
120
|
+
uri = URI.parse(dsn.to_s.strip)
|
|
121
|
+
project = uri.path.to_s.sub(%r{\A/+}, '')
|
|
122
|
+
return nil if uri.host.nil? || uri.host.empty? || project.empty? || uri.user.nil? || uri.user.empty?
|
|
123
|
+
|
|
124
|
+
{ host: uri.host, project_id: project, public_key: uri.user, secret_key: uri.password }
|
|
125
|
+
rescue URI::Error
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# --- public settings --------------------------------------------------
|
|
131
|
+
|
|
132
|
+
# Turn error and performance reporting on (default) or off.
|
|
133
|
+
def set_error_reporting_enabled(enabled)
|
|
134
|
+
@error_reporting_enabled = enabled ? true : false
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Minimal mode: only error code/message, endpoint, timing, versions and
|
|
139
|
+
# the reseller ID; no request/response data, domain or server IP.
|
|
140
|
+
def set_telemetry_minimal(minimal)
|
|
141
|
+
@telemetry_minimal = minimal ? true : false
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Override the Sentry DSN ('' disables telemetry).
|
|
146
|
+
def set_sentry_dsn(dsn)
|
|
147
|
+
@sentry_dsn = dsn.to_s
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def get_sentry_dsn
|
|
152
|
+
@sentry_dsn
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Replace the telemetry sender (see THREAD_SENDER).
|
|
156
|
+
def set_telemetry_sender(sender)
|
|
157
|
+
@telemetry_sender = sender
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
def telemetry_minimal?
|
|
164
|
+
@telemetry_minimal ? true : false
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def send_error_to_sentry_async(error)
|
|
168
|
+
do_send_error_to_sentry(error)
|
|
169
|
+
rescue StandardError, ScriptError
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def do_send_error_to_sentry(error)
|
|
174
|
+
return unless @error_reporting_enabled
|
|
175
|
+
|
|
176
|
+
message = error.message.to_s
|
|
177
|
+
return if DEFAULT_IGNORED_ERRORS.any? { |ev| message.include?(ev) }
|
|
178
|
+
|
|
179
|
+
dsn = Telemetry.parse_dsn(@sentry_dsn)
|
|
180
|
+
return unless dsn
|
|
181
|
+
|
|
182
|
+
elapsed = @start_at ? monotonic - @start_at : 0.0
|
|
183
|
+
classification = build_sentry_classification(error)
|
|
184
|
+
frames = telemetry_frames(error)
|
|
185
|
+
culprit = "#{self.class.name} via #{File.basename(__FILE__)}"
|
|
186
|
+
|
|
187
|
+
event = {
|
|
188
|
+
'event_id' => SecureRandom.hex(16),
|
|
189
|
+
'timestamp' => Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
190
|
+
'level' => 'error',
|
|
191
|
+
'logger' => 'ruby',
|
|
192
|
+
'platform' => 'ruby',
|
|
193
|
+
'culprit' => culprit,
|
|
194
|
+
'message' => { 'formatted' => classification['value'] },
|
|
195
|
+
'exception' => {
|
|
196
|
+
'values' => [
|
|
197
|
+
{
|
|
198
|
+
'type' => "#{APPLICATION} Exception",
|
|
199
|
+
'value' => classification['value'],
|
|
200
|
+
'stacktrace' => { 'frames' => frames }
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
},
|
|
204
|
+
'tags' => {
|
|
205
|
+
'handled' => 'yes',
|
|
206
|
+
'level' => 'error',
|
|
207
|
+
'url' => 'NA',
|
|
208
|
+
'transaction' => 'NA',
|
|
209
|
+
'trace_id' => SecureRandom.hex(8),
|
|
210
|
+
'runtime_name' => 'Ruby',
|
|
211
|
+
'runtime_version' => RUBY_VERSION,
|
|
212
|
+
'elapsed_time' => Php.number_format(elapsed, 4),
|
|
213
|
+
'application' => APPLICATION,
|
|
214
|
+
'library_version' => VERSION,
|
|
215
|
+
'ip_address' => '',
|
|
216
|
+
'vhost_user' => telemetry_minimal? ? '' : vhost_user
|
|
217
|
+
},
|
|
218
|
+
'extra' => {
|
|
219
|
+
'request_data' => telemetry_mask(@last_request),
|
|
220
|
+
'response_data' => telemetry_mask(@last_response),
|
|
221
|
+
'openssl_v' => defined?(OpenSSL::OPENSSL_VERSION) ? OpenSSL::OPENSSL_VERSION : 'NA',
|
|
222
|
+
'openssl_n' => defined?(OpenSSL::OPENSSL_VERSION_NUMBER) ? OpenSSL::OPENSSL_VERSION_NUMBER : 'NA'
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
event['release'] = VERSION
|
|
227
|
+
event['environment'] = telemetry_environment
|
|
228
|
+
event['tags'] = event['tags'].merge(classification['tags'])
|
|
229
|
+
event['extra']['original_message'] = message
|
|
230
|
+
event['fingerprint'] = classification['fingerprint']
|
|
231
|
+
|
|
232
|
+
if telemetry_minimal?
|
|
233
|
+
%w[url ip_address vhost_user ip_value].each { |k| event['tags'].delete(k) }
|
|
234
|
+
lr = @last_request.is_a?(Hash) ? @last_request : {}
|
|
235
|
+
endpoint = begin
|
|
236
|
+
URI.parse(Php.to_s(lr['url'])).path.to_s
|
|
237
|
+
rescue URI::Error
|
|
238
|
+
''
|
|
239
|
+
end
|
|
240
|
+
event['extra']['request_data'] = { 'method' => Php.to_s(lr['method']), 'endpoint' => endpoint }
|
|
241
|
+
event['extra']['response_data'] = { 'http_code' => Php.to_i(Php.get(@last_response_headers, 'http_code')) }
|
|
242
|
+
event['exception']['values'].each do |value|
|
|
243
|
+
value['stacktrace']['frames'].each { |f| f['filename'] = File.basename(f['filename'].to_s) }
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
auth = ['sentry_version=7', "sentry_client=ruby-dna/#{VERSION}", "sentry_key=#{dsn[:public_key]}"]
|
|
248
|
+
auth << "sentry_secret=#{dsn[:secret_key]}" if dsn[:secret_key] && !dsn[:secret_key].empty?
|
|
249
|
+
url = "https://#{dsn[:host]}/api/#{dsn[:project_id]}/store/"
|
|
250
|
+
sender.call(url, event, "X-Sentry-Auth: Sentry #{auth.join(', ')}", !telemetry_minimal?)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def send_performance_metrics_to_sentry(metrics)
|
|
254
|
+
return unless @error_reporting_enabled
|
|
255
|
+
|
|
256
|
+
dsn = Telemetry.parse_dsn(@sentry_dsn)
|
|
257
|
+
return unless dsn
|
|
258
|
+
|
|
259
|
+
trace_id = session_trace_id
|
|
260
|
+
root_span_id = SecureRandom.hex(8)
|
|
261
|
+
child_span_id = SecureRandom.hex(8)
|
|
262
|
+
op = 'http.client'
|
|
263
|
+
|
|
264
|
+
api_code = nil
|
|
265
|
+
if @last_parsed_response.is_a?(Hash) && @last_parsed_response['Code'].is_a?(String)
|
|
266
|
+
api_code = @last_parsed_response['Code']
|
|
267
|
+
elsif Php.get(@last_response, 'error', 'code').is_a?(String)
|
|
268
|
+
api_code = Php.get(@last_response, 'error', 'code')
|
|
269
|
+
end
|
|
270
|
+
trace_status = map_trace_status(metrics['success'] ? true : false, api_code)
|
|
271
|
+
|
|
272
|
+
common = build_common_tags(nil)
|
|
273
|
+
common['api_code'] = api_code.to_s if !api_code.nil? && !common.key?('api_code')
|
|
274
|
+
http_code = Php.to_i(Php.get(@last_response_headers, 'http_code'))
|
|
275
|
+
common['status_code'] = http_code.to_s if http_code != 0
|
|
276
|
+
|
|
277
|
+
http_method = 'GET'
|
|
278
|
+
lr = @last_request.is_a?(Hash) ? @last_request : {}
|
|
279
|
+
http_method = Php.to_s(lr['method']) unless Php.empty?(lr['method'])
|
|
280
|
+
call_url = Php.empty?(lr['url']) ? '' : Php.to_s(lr['url'])
|
|
281
|
+
call_host = host_of(call_url.empty? ? @service_url : call_url)
|
|
282
|
+
|
|
283
|
+
span_data = { 'http.request.method' => http_method }
|
|
284
|
+
span_data['server.address'] = call_host unless call_host.empty?
|
|
285
|
+
span_data['url.full'] = call_url.split('?', 2).first if !call_url.empty? && !telemetry_minimal?
|
|
286
|
+
span_data['http.response.status_code'] = common['status_code'].to_i if common.key?('status_code')
|
|
287
|
+
description = call_host.empty? ? metrics['operation'].to_s : "#{http_method} https://#{call_host}"
|
|
288
|
+
|
|
289
|
+
event = {
|
|
290
|
+
'event_id' => SecureRandom.hex(16),
|
|
291
|
+
'platform' => 'ruby',
|
|
292
|
+
'level' => 'info',
|
|
293
|
+
'type' => 'transaction',
|
|
294
|
+
'transaction' => "API.#{metrics['operation']}",
|
|
295
|
+
'transaction_info' => { 'source' => 'custom' },
|
|
296
|
+
'release' => VERSION,
|
|
297
|
+
'environment' => telemetry_environment,
|
|
298
|
+
'timestamp' => metrics['timestamp'],
|
|
299
|
+
'start_timestamp' => metrics['start_timestamp'],
|
|
300
|
+
'contexts' => {
|
|
301
|
+
'trace' => { 'trace_id' => trace_id, 'span_id' => root_span_id, 'op' => op, 'status' => trace_status },
|
|
302
|
+
'runtime' => { 'name' => 'ruby', 'version' => RUBY_VERSION }
|
|
303
|
+
},
|
|
304
|
+
'tags' => {
|
|
305
|
+
'application' => APPLICATION,
|
|
306
|
+
'library_version' => VERSION,
|
|
307
|
+
'ip_address' => '',
|
|
308
|
+
'vhost_user' => telemetry_minimal? ? '' : vhost_user
|
|
309
|
+
}.merge(common),
|
|
310
|
+
'measurements' => {
|
|
311
|
+
'duration' => { 'value' => metrics['duration'].to_f, 'unit' => 'millisecond' }
|
|
312
|
+
},
|
|
313
|
+
'spans' => [
|
|
314
|
+
{
|
|
315
|
+
'span_id' => child_span_id,
|
|
316
|
+
'parent_span_id' => root_span_id,
|
|
317
|
+
'trace_id' => trace_id,
|
|
318
|
+
'op' => op,
|
|
319
|
+
'description' => description,
|
|
320
|
+
'status' => trace_status,
|
|
321
|
+
'data' => span_data,
|
|
322
|
+
'start_timestamp' => metrics['start_timestamp'],
|
|
323
|
+
'timestamp' => metrics['timestamp']
|
|
324
|
+
}
|
|
325
|
+
]
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
auth = ['sentry_version=7', "sentry_client=ruby-dna/#{VERSION}", "sentry_key=#{dsn[:public_key]}"]
|
|
329
|
+
auth << "sentry_secret=#{dsn[:secret_key]}" if dsn[:secret_key] && !dsn[:secret_key].empty?
|
|
330
|
+
url = "https://#{dsn[:host]}/api/#{dsn[:project_id]}/store/"
|
|
331
|
+
sender.call(url, event, "X-Sentry-Auth: Sentry #{auth.join(', ')}", !telemetry_minimal?)
|
|
332
|
+
rescue StandardError, ScriptError
|
|
333
|
+
nil
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def sender
|
|
337
|
+
@telemetry_sender || THREAD_SENDER
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Mask credentials (auth/EPP codes, API keys, passwords, tokens) as ***.
|
|
341
|
+
def telemetry_mask(data, depth = 0)
|
|
342
|
+
return '[…]' if depth > 8
|
|
343
|
+
|
|
344
|
+
case data
|
|
345
|
+
when Hash
|
|
346
|
+
data.each_with_object({}) do |(k, v), out|
|
|
347
|
+
out[k] = if MASK_KEY_RE.match?(k.to_s)
|
|
348
|
+
v.nil? || v == '' ? v : '***'
|
|
349
|
+
else
|
|
350
|
+
telemetry_mask(v, depth + 1)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
when Array
|
|
354
|
+
data.map { |v| telemetry_mask(v, depth + 1) }
|
|
355
|
+
else
|
|
356
|
+
data
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# File, line and method names only, oldest first; never arguments.
|
|
361
|
+
def telemetry_frames(error)
|
|
362
|
+
lines = error.backtrace || caller(3) || []
|
|
363
|
+
frames = lines.reverse.map do |line|
|
|
364
|
+
m = line.to_s.match(/\A(.*?):(\d+)(?::in [`'](.*)')?\z/)
|
|
365
|
+
if m
|
|
366
|
+
{ 'filename' => File.basename(m[1]), 'lineno' => m[2].to_i, 'function' => m[3].to_s }
|
|
367
|
+
else
|
|
368
|
+
{ 'filename' => '', 'lineno' => 0, 'function' => line.to_s }
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
frames << { 'filename' => File.basename(__FILE__), 'lineno' => 0, 'function' => '' } if frames.empty?
|
|
372
|
+
frames
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def build_common_tags(error)
|
|
376
|
+
tags = {}
|
|
377
|
+
tags['reseller'] = @reseller_id.to_s unless Php.empty?(@reseller_id)
|
|
378
|
+
|
|
379
|
+
domain = Php.get(@last_request, 'payload', 'domainName')
|
|
380
|
+
tags['domain'] = domain if domain.is_a?(String) && !Php.empty?(domain) && !telemetry_minimal?
|
|
381
|
+
|
|
382
|
+
tags['operation'] = @last_function.to_s unless Php.empty?(@last_function)
|
|
383
|
+
tags['api_type'] = 'REST'
|
|
384
|
+
|
|
385
|
+
if error
|
|
386
|
+
msg = error.message.to_s
|
|
387
|
+
code = error.respond_to?(:code) ? error.code : 0
|
|
388
|
+
if (m = msg.match(/API_(-?\d+)_ERROR/))
|
|
389
|
+
tags['api_code'] = "API_#{m[1]}"
|
|
390
|
+
elsif Php.numeric?(code) && Php.to_i(code) != 0
|
|
391
|
+
tags['api_code'] = "API_#{Php.to_i(code)}"
|
|
392
|
+
end
|
|
393
|
+
tags['status_code'] = Php.to_i(code).to_s if Php.numeric?(code) && Php.to_i(code) != 0
|
|
394
|
+
end
|
|
395
|
+
tags
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def map_trace_status(success, api_code)
|
|
399
|
+
return 'ok' if success
|
|
400
|
+
return 'unknown' if api_code.nil?
|
|
401
|
+
|
|
402
|
+
m = api_code.to_s.match(/-?\d+/)
|
|
403
|
+
return 'unknown' unless m
|
|
404
|
+
|
|
405
|
+
code = m[0].to_i
|
|
406
|
+
return 'unauthenticated' if [401, 2202].include?(code)
|
|
407
|
+
return 'permission_denied' if code == 403
|
|
408
|
+
return 'not_found' if [404, 510].include?(code)
|
|
409
|
+
return 'deadline_exceeded' if [408, 504].include?(code)
|
|
410
|
+
return 'unavailable' if code == -1
|
|
411
|
+
return 'internal_error' if code >= 500 && code < 600
|
|
412
|
+
return 'invalid_argument' if code >= 400 && code < 500
|
|
413
|
+
|
|
414
|
+
'unknown'
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def telemetry_environment
|
|
418
|
+
@service_url.to_s.downcase.include?('ote.') ? 'ote' : 'production'
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def session_trace_id
|
|
422
|
+
@session_trace_id ||= SecureRandom.hex(16)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def build_sentry_classification(error)
|
|
426
|
+
msg = error.message.to_s
|
|
427
|
+
tags = build_common_tags(error)
|
|
428
|
+
if (m = msg.match(/\b(\d{1,3}(?:\.\d{1,3}){3})\b/))
|
|
429
|
+
tags['ip_value'] = m[1]
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
api_code = tags['api_code']
|
|
433
|
+
n = msg.dup
|
|
434
|
+
n = n.sub(/\A\[API_ERROR\]:\s*/i, '')
|
|
435
|
+
n = n.gsub(/API_-?\d+_ERROR\s*-\s*Failed\s*-\s*/i, '')
|
|
436
|
+
n = n.gsub(/\s*\(Reseller Id[^)]*\)\s*/i, '')
|
|
437
|
+
n = n.gsub(/\b\d{1,3}(?:\.\d{1,3}){3}\b/, '{ip}')
|
|
438
|
+
n = n.gsub(/'[^']{0,80}'|"[^"]{0,80}"/, '{v}')
|
|
439
|
+
n = n.gsub(/\b\d{2,}\b/, '{n}')
|
|
440
|
+
n = n.gsub(/\s+/, ' ').strip
|
|
441
|
+
|
|
442
|
+
if n.empty?
|
|
443
|
+
n = api_code || error.class.name.split('::').last || 'Exception'
|
|
444
|
+
elsif api_code && !n.start_with?(api_code)
|
|
445
|
+
n = "#{api_code}: #{n}"
|
|
446
|
+
end
|
|
447
|
+
n = "#{n.byteslice(0, 157).scrub('')}..." if n.bytesize > 160
|
|
448
|
+
|
|
449
|
+
{
|
|
450
|
+
'value' => n,
|
|
451
|
+
'tags' => tags,
|
|
452
|
+
'fingerprint' => ['dna', APPLICATION, tags['operation'] || 'unknown', n]
|
|
453
|
+
}
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def vhost_user
|
|
457
|
+
ENV['USER'] || ENV['USERNAME'] || ''
|
|
458
|
+
rescue StandardError
|
|
459
|
+
''
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def host_of(url)
|
|
463
|
+
URI.parse(url.to_s).host.to_s
|
|
464
|
+
rescue URI::Error
|
|
465
|
+
''
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def monotonic
|
|
469
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
require 'timeout'
|
|
6
|
+
require 'uri'
|
|
7
|
+
|
|
8
|
+
module DomainNameApi
|
|
9
|
+
# Raised by a transport when the request never produced an HTTP response
|
|
10
|
+
# (timeout, DNS failure, TLS error, connection reset). `code` follows the
|
|
11
|
+
# curl error numbers php-dna reports (28 = timeout, 6 = DNS, 7 = connect,
|
|
12
|
+
# 35/60 = TLS, 52 = empty reply, 56 = receive failure).
|
|
13
|
+
class TransportError < StandardError
|
|
14
|
+
attr_reader :code
|
|
15
|
+
|
|
16
|
+
def initialize(message, code = 56)
|
|
17
|
+
super(message)
|
|
18
|
+
@code = code
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def timeout?
|
|
22
|
+
code == 28
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# An HTTP response as seen by the client.
|
|
27
|
+
Response = Struct.new(:status, :body, :headers, keyword_init: true)
|
|
28
|
+
|
|
29
|
+
# Default transport: Net::HTTP from the standard library.
|
|
30
|
+
#
|
|
31
|
+
# Any object responding to `call(request)` can replace it (see
|
|
32
|
+
# Client#set_transport). `request` is a Hash with :method, :url, :headers,
|
|
33
|
+
# :body (String or nil), :timeout, :connect_timeout and :verify_ssl; the
|
|
34
|
+
# result must be a Response (or a Hash with :status, :body, :headers), or
|
|
35
|
+
# the transport raises TransportError.
|
|
36
|
+
class NetHttpTransport
|
|
37
|
+
def call(request)
|
|
38
|
+
uri = URI.parse(request[:url])
|
|
39
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
40
|
+
http.use_ssl = uri.scheme == 'https'
|
|
41
|
+
http.verify_mode = request[:verify_ssl] == false ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
|
|
42
|
+
http.open_timeout = request[:connect_timeout]
|
|
43
|
+
http.read_timeout = request[:timeout]
|
|
44
|
+
http.write_timeout = request[:timeout] if http.respond_to?(:write_timeout=)
|
|
45
|
+
http.ssl_timeout = request[:connect_timeout] if http.use_ssl?
|
|
46
|
+
|
|
47
|
+
req = build_request(request[:method], uri, request[:headers], request[:body])
|
|
48
|
+
res = with_deadline(request[:timeout]) { http.start { |h| h.request(req) } }
|
|
49
|
+
|
|
50
|
+
headers = {}
|
|
51
|
+
res.each_header { |k, v| headers[k] = v }
|
|
52
|
+
Response.new(status: res.code.to_i, body: res.body.to_s.dup.force_encoding(Encoding::UTF_8), headers: headers)
|
|
53
|
+
rescue TransportError
|
|
54
|
+
raise
|
|
55
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Timeout::Error => e
|
|
56
|
+
raise TransportError.new(e.message, 28)
|
|
57
|
+
rescue SocketError => e
|
|
58
|
+
raise TransportError.new(e.message, 6)
|
|
59
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH => e
|
|
60
|
+
raise TransportError.new(e.message, 7)
|
|
61
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
62
|
+
raise TransportError.new(e.message, e.message =~ /certificate/i ? 60 : 35)
|
|
63
|
+
rescue EOFError => e
|
|
64
|
+
raise TransportError.new(e.message, 52)
|
|
65
|
+
rescue IOError, SystemCallError => e
|
|
66
|
+
raise TransportError.new(e.message, 56)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def build_request(method, uri, headers, body)
|
|
72
|
+
klass = {
|
|
73
|
+
'GET' => Net::HTTP::Get, 'POST' => Net::HTTP::Post,
|
|
74
|
+
'PUT' => Net::HTTP::Put, 'DELETE' => Net::HTTP::Delete
|
|
75
|
+
}.fetch(method) { raise ArgumentError, "unsupported HTTP method #{method}" }
|
|
76
|
+
req = klass.new(uri.request_uri)
|
|
77
|
+
headers.each { |k, v| req[k] = v }
|
|
78
|
+
req.body = body unless body.nil?
|
|
79
|
+
req
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Total-time limit (curl CURLOPT_TIMEOUT) on top of the per-phase ones.
|
|
83
|
+
def with_deadline(seconds, &block)
|
|
84
|
+
return yield if seconds.nil? || seconds.to_f <= 0
|
|
85
|
+
|
|
86
|
+
Timeout.timeout(seconds.to_f, &block)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'domainnameapi/version'
|
|
4
|
+
require_relative 'domainnameapi/php'
|
|
5
|
+
require_relative 'domainnameapi/transport'
|
|
6
|
+
require_relative 'domainnameapi/telemetry'
|
|
7
|
+
require_relative 'domainnameapi/rest'
|
|
8
|
+
require_relative 'domainnameapi/client'
|
|
9
|
+
|
|
10
|
+
# Ruby client for the DomainNameAPI domain reseller REST API.
|
|
11
|
+
#
|
|
12
|
+
# require 'domainnameapi'
|
|
13
|
+
# dna = DomainNameApi::Client.new('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', 'your-api-key')
|
|
14
|
+
# dna.get_details('example.com')
|
|
15
|
+
module DomainNameApi
|
|
16
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: domainnameapi
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- DomainNameAPI
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Register, renew, transfer and manage domains, contacts, child nameservers,
|
|
13
|
+
DNS zones and URL forwarding through the DomainNameAPI REST gateway. A faithful
|
|
14
|
+
port of php-dna with zero runtime dependencies.
|
|
15
|
+
email:
|
|
16
|
+
- support@domainnameapi.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- CHANGELOG.md
|
|
22
|
+
- LICENSE
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/domainnameapi.rb
|
|
25
|
+
- lib/domainnameapi/client.rb
|
|
26
|
+
- lib/domainnameapi/php.rb
|
|
27
|
+
- lib/domainnameapi/rest.rb
|
|
28
|
+
- lib/domainnameapi/telemetry.rb
|
|
29
|
+
- lib/domainnameapi/transport.rb
|
|
30
|
+
- lib/domainnameapi/version.rb
|
|
31
|
+
homepage: https://github.com/domainreseller/ruby-dna
|
|
32
|
+
licenses:
|
|
33
|
+
- Apache-2.0
|
|
34
|
+
metadata:
|
|
35
|
+
homepage_uri: https://github.com/domainreseller/ruby-dna
|
|
36
|
+
source_code_uri: https://github.com/domainreseller/ruby-dna
|
|
37
|
+
changelog_uri: https://github.com/domainreseller/ruby-dna/blob/main/CHANGELOG.md
|
|
38
|
+
bug_tracker_uri: https://github.com/domainreseller/ruby-dna/issues
|
|
39
|
+
rdoc_options: []
|
|
40
|
+
require_paths:
|
|
41
|
+
- lib
|
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.7'
|
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
|
+
requirements:
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '0'
|
|
52
|
+
requirements: []
|
|
53
|
+
rubygems_version: 3.6.9
|
|
54
|
+
specification_version: 4
|
|
55
|
+
summary: Ruby client for the DomainNameAPI domain reseller REST API.
|
|
56
|
+
test_files: []
|