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.
@@ -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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DomainNameApi
4
+ # Library version, reported in the User-Agent and in telemetry.
5
+ VERSION = '1.0.0'
6
+ 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: []