opensrs-ruby 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6fa12db38773abeaa598352a9c011222163f51fb764d9211011f2e9cdd0bd87f
4
+ data.tar.gz: de281c57faf8f23c228416cf9f3484713581f532d2020e2755e0564e831b3eb4
5
+ SHA512:
6
+ metadata.gz: c3dd656f403f0d9312cd2531b97c63d431fab45e1839c3c8b3759e84b0da83fdd2ad79a35311392d773e426223d689d05c300af5a156c9697d7ec3e33a849b5d
7
+ data.tar.gz: d915f09dca9694ed8aa54d5f867ffd3db3be858b1036295680f41f9e7d5bdf1a443e6ac1477f672b8ffe45540fd45cfc13d2f3d64b2b250ac6076679cffd12cc
@@ -0,0 +1,16 @@
1
+ module OpenSRS
2
+ Balance = Struct.new(:amount, :hold, keyword_init: true)
3
+
4
+ module Account
5
+ module_function
6
+
7
+ def balance(client: OpenSRS.default_client)
8
+ result = client.call(action: "GET_BALANCE", object: "BALANCE")
9
+ attrs = result["attributes"]
10
+ Balance.new(
11
+ amount: attrs["balance"].to_f,
12
+ hold: attrs["hold_balance"].to_f
13
+ )
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,174 @@
1
+ require "net/http"
2
+ require "uri"
3
+ require "digest/md5"
4
+ require "rexml/document"
5
+
6
+ module OpenSRS
7
+ class Client
8
+ LIVE_URL = "https://rr-n1-tor.opensrs.net:55443"
9
+ TEST_URL = "https://horizon.opensrs.net:55443"
10
+
11
+ attr_reader :username
12
+
13
+ def initialize(username:, api_key:, test: false)
14
+ @username = username
15
+ @api_key = api_key
16
+ @url = test ? TEST_URL : LIVE_URL
17
+ end
18
+
19
+ def call(action:, object:, attributes: {}, extra: {})
20
+ xml = build_xml(action, object, attributes, extra)
21
+ response_body = post(xml)
22
+ result = parse_response(response_body)
23
+ check_for_errors!(result)
24
+ result
25
+ end
26
+
27
+ private
28
+
29
+ def post(xml)
30
+ signature = compute_signature(xml)
31
+ uri = URI(@url)
32
+ http = Net::HTTP.new(uri.host, uri.port)
33
+ http.use_ssl = true
34
+ http.open_timeout = 15
35
+ http.read_timeout = 30
36
+
37
+ request = Net::HTTP::Post.new("/")
38
+ request["Content-Type"] = "text/xml"
39
+ request["X-Username"] = @username
40
+ request["X-Signature"] = signature
41
+ request.body = xml
42
+
43
+ http.request(request).body
44
+ rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, Errno::ECONNRESET => e
45
+ raise ConnectionError, e.message
46
+ end
47
+
48
+ def compute_signature(xml)
49
+ step1 = Digest::MD5.hexdigest(xml + @api_key)
50
+ Digest::MD5.hexdigest(step1 + @api_key)
51
+ end
52
+
53
+ def check_for_errors!(result)
54
+ return if result["is_success"] == "1"
55
+
56
+ code = result["response_code"]
57
+ message = result["response_text"]
58
+
59
+ case code
60
+ when "415"
61
+ raise AuthenticationError.new(message, code)
62
+ when "485"
63
+ raise InsufficientBalance.new(message, code)
64
+ when "211"
65
+ raise DomainUnavailable.new(message, code)
66
+ when "487"
67
+ raise TransferError.new(message, code)
68
+ when "410"
69
+ raise DomainNotFound.new(message, code)
70
+ else
71
+ raise RequestError.new(message, code)
72
+ end
73
+ end
74
+
75
+ def build_xml(action, object, attributes, extra)
76
+ items = [
77
+ item("protocol", "XCP"),
78
+ item("action", action),
79
+ item("object", object),
80
+ ]
81
+ extra.each { |k, v| items << item(k, v) }
82
+ items << item_assoc("attributes", attributes) unless attributes.empty?
83
+
84
+ <<~XML
85
+ <?xml version='1.0' encoding='UTF-8' standalone='no' ?>
86
+ <!DOCTYPE OPS_envelope SYSTEM 'ops.dtd'>
87
+ <OPS_envelope>
88
+ <header><version>0.9</version></header>
89
+ <body>
90
+ <data_block>
91
+ <dt_assoc>
92
+ #{items.join("\n ")}
93
+ </dt_assoc>
94
+ </data_block>
95
+ </body>
96
+ </OPS_envelope>
97
+ XML
98
+ end
99
+
100
+ def item(key, value)
101
+ "<item key=\"#{key}\">#{escape(value)}</item>"
102
+ end
103
+
104
+ def item_assoc(key, hash)
105
+ inner = hash.map do |k, v|
106
+ if v.is_a?(Hash)
107
+ item_assoc(k, v)
108
+ elsif v.is_a?(Array)
109
+ item_array(k, v)
110
+ else
111
+ item(k, v)
112
+ end
113
+ end
114
+ "<item key=\"#{key}\"><dt_assoc>#{inner.join}</dt_assoc></item>"
115
+ end
116
+
117
+ def item_array(key, arr)
118
+ inner = arr.each_with_index.map do |v, i|
119
+ if v.is_a?(Hash)
120
+ item_assoc(i.to_s, v)
121
+ else
122
+ item(i.to_s, v)
123
+ end
124
+ end
125
+ "<item key=\"#{key}\"><dt_array>#{inner.join}</dt_array></item>"
126
+ end
127
+
128
+ def escape(val)
129
+ val.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;")
130
+ end
131
+
132
+ def parse_response(xml_str)
133
+ doc = REXML::Document.new(xml_str)
134
+ root_assoc = doc.elements["//body/data_block/dt_assoc"]
135
+ parse_assoc(root_assoc)
136
+ end
137
+
138
+ def parse_assoc(element)
139
+ return {} unless element
140
+ result = {}
141
+ element.elements.each("item") do |el|
142
+ key = el.attributes["key"]
143
+ child_assoc = el.elements["dt_assoc"]
144
+ child_array = el.elements["dt_array"]
145
+ if child_assoc
146
+ result[key] = parse_assoc(child_assoc)
147
+ elsif child_array
148
+ result[key] = parse_array(child_array)
149
+ else
150
+ result[key] = el.text&.strip
151
+ end
152
+ end
153
+ result
154
+ end
155
+
156
+ def parse_array(element)
157
+ return [] unless element
158
+ items = {}
159
+ element.elements.each("item") do |el|
160
+ key = el.attributes["key"].to_i
161
+ child_assoc = el.elements["dt_assoc"]
162
+ child_array = el.elements["dt_array"]
163
+ if child_assoc
164
+ items[key] = parse_assoc(child_assoc)
165
+ elsif child_array
166
+ items[key] = parse_array(child_array)
167
+ else
168
+ items[key] = el.text&.strip
169
+ end
170
+ end
171
+ items.sort_by(&:first).map(&:last)
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,15 @@
1
+ module OpenSRS
2
+ Contact = Struct.new(
3
+ :first_name, :last_name, :org_name, :address1, :address2,
4
+ :city, :state, :postal_code, :country, :phone, :email,
5
+ keyword_init: true
6
+ ) do
7
+ def self.from_hash(hash)
8
+ new(**hash.transform_keys(&:to_sym).slice(*members))
9
+ end
10
+
11
+ def to_api_hash
12
+ to_h.compact.transform_keys(&:to_s)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,278 @@
1
+ require "time"
2
+ require "digest/md5"
3
+
4
+ module OpenSRS
5
+ DomainCheck = Struct.new(:domain, :available, :price, keyword_init: true)
6
+ DomainSuggestion = Struct.new(:name, :available, keyword_init: true)
7
+
8
+ class Domain
9
+ attr_reader :name, :expires_at, :created_at, :updated_at, :nameservers, :contacts
10
+
11
+ def initialize(name:, client: nil, expires_at: nil, created_at: nil,
12
+ updated_at: nil, auto_renew: false, locked: false,
13
+ whois_private: false, nameservers: [], contacts: {})
14
+ @name = name
15
+ @client = client || OpenSRS.default_client
16
+ @expires_at = expires_at
17
+ @created_at = created_at
18
+ @updated_at = updated_at
19
+ @auto_renew = auto_renew
20
+ @locked = locked
21
+ @whois_private = whois_private
22
+ @nameservers = nameservers
23
+ @contacts = contacts
24
+ end
25
+
26
+ def auto_renew? = @auto_renew
27
+ def locked? = @locked
28
+ def whois_private? = @whois_private
29
+
30
+ class << self
31
+ def available?(domain_name, client: OpenSRS.default_client)
32
+ result = client.call(action: "LOOKUP", object: "DOMAIN",
33
+ attributes: { "domain" => domain_name })
34
+ result.dig("attributes", "status") == "available"
35
+ rescue OpenSRS::DomainUnavailable
36
+ false
37
+ end
38
+
39
+ def check(domain_name, client: OpenSRS.default_client)
40
+ lookup = client.call(action: "LOOKUP", object: "DOMAIN",
41
+ attributes: { "domain" => domain_name })
42
+ available = lookup.dig("attributes", "status") == "available"
43
+
44
+ price = nil
45
+ if available
46
+ price_result = client.call(action: "GET_PRICE", object: "DOMAIN",
47
+ attributes: { "domain" => domain_name, "period" => "1", "reg_type" => "new" })
48
+ price = price_result.dig("attributes", "price")&.to_f
49
+ end
50
+ price ||= lookup.dig("attributes", "price")&.to_f
51
+
52
+ DomainCheck.new(domain: domain_name, available: available, price: price)
53
+ rescue OpenSRS::DomainUnavailable
54
+ DomainCheck.new(domain: domain_name, available: false, price: nil)
55
+ end
56
+
57
+ def price(domain_name, period: 1, client: OpenSRS.default_client)
58
+ result = client.call(action: "GET_PRICE", object: "DOMAIN",
59
+ attributes: { "domain" => domain_name, "period" => period.to_s, "reg_type" => "new" })
60
+ result.dig("attributes", "price").to_f
61
+ end
62
+
63
+ def suggest(search_string, tlds: [".com", ".net", ".org", ".io"], client: OpenSRS.default_client)
64
+ result = client.call(action: "NAME_SUGGEST", object: "DOMAIN",
65
+ attributes: { "searchstring" => search_string, "tlds" => tlds, "max_wait_time" => "5" })
66
+ attrs = result["attributes"] || {}
67
+
68
+ suggestions = []
69
+ %w[lookup suggestion].each do |category|
70
+ items = attrs.dig(category, "items")
71
+ next unless items.is_a?(Array)
72
+ items.each do |item|
73
+ next unless item.is_a?(Hash) && item["domain"]
74
+ suggestions << DomainSuggestion.new(
75
+ name: item["domain"],
76
+ available: item["status"] == "available"
77
+ )
78
+ end
79
+ end
80
+ suggestions
81
+ end
82
+
83
+ def find(domain_name, client: OpenSRS.default_client)
84
+ result = client.call(action: "GET", object: "DOMAIN",
85
+ attributes: { "type" => "all_info" },
86
+ extra: { "domain" => domain_name })
87
+ attrs = result["attributes"] || {}
88
+ from_api(domain_name, attrs, client: client)
89
+ end
90
+
91
+ def all(from: "2020-01-01", to: "2030-12-31", client: OpenSRS.default_client)
92
+ result = client.call(action: "GET_DOMAINS_BY_EXPIREDATE", object: "DOMAIN",
93
+ attributes: { "exp_from" => from, "exp_to" => to, "limit" => "200", "page" => "0" })
94
+ domains = result.dig("attributes", "exp_domains") || []
95
+ domains.map do |d|
96
+ new(
97
+ name: d["name"],
98
+ expires_at: parse_time(d["expiredate"]),
99
+ client: client
100
+ )
101
+ end
102
+ end
103
+
104
+ def transferable?(domain_name, client: OpenSRS.default_client)
105
+ result = client.call(action: "CHECK_TRANSFER", object: "DOMAIN",
106
+ attributes: { "domain" => domain_name, "check_status" => "1" })
107
+ result.dig("attributes", "transferrable") == "1"
108
+ end
109
+
110
+ def register!(domain_name, period: 1, contacts:, nameservers:, client: OpenSRS.default_client)
111
+ contact_set = {}
112
+ contacts.each do |role, data|
113
+ c = data.is_a?(Contact) ? data : Contact.new(**data)
114
+ contact_set[role.to_s] = c.to_api_hash
115
+ end
116
+ %w[admin billing].each { |r| contact_set[r] ||= contact_set["owner"] }
117
+
118
+ ns_list = nameservers.each_with_index.map do |ns, i|
119
+ { "name" => ns, "sortorder" => (i + 1).to_s }
120
+ end
121
+
122
+ reg_user = domain_name.gsub(/[^a-z0-9]/i, "")[0, 20]
123
+ reg_pass = Digest::MD5.hexdigest(domain_name + Time.now.to_s)[0, 20]
124
+
125
+ client.call(action: "SW_REGISTER", object: "DOMAIN", attributes: {
126
+ "domain" => domain_name,
127
+ "reg_type" => "new",
128
+ "period" => period.to_s,
129
+ "reg_username" => reg_user,
130
+ "reg_password" => reg_pass,
131
+ "handle" => "process",
132
+ "custom_nameservers" => "1",
133
+ "custom_tech_contact" => "0",
134
+ "auto_renew" => "0",
135
+ "f_whois_privacy" => "1",
136
+ "contact_set" => contact_set,
137
+ "nameserver_list" => ns_list,
138
+ })
139
+
140
+ new(name: domain_name, client: client, nameservers: nameservers)
141
+ end
142
+
143
+ def transfer!(domain_name, auth_code:, contacts:, nameservers: [], client: OpenSRS.default_client)
144
+ contact_set = {}
145
+ contacts.each do |role, data|
146
+ c = data.is_a?(Contact) ? data : Contact.new(**data)
147
+ contact_set[role.to_s] = c.to_api_hash
148
+ end
149
+ %w[admin billing].each { |r| contact_set[r] ||= contact_set["owner"] }
150
+
151
+ reg_user = domain_name.gsub(/[^a-z0-9]/i, "")[0, 20]
152
+ reg_pass = Digest::MD5.hexdigest(domain_name + Time.now.to_s)[0, 20]
153
+
154
+ attrs = {
155
+ "domain" => domain_name,
156
+ "reg_type" => "transfer",
157
+ "period" => "1",
158
+ "reg_username" => reg_user,
159
+ "reg_password" => reg_pass,
160
+ "handle" => "process",
161
+ "custom_nameservers" => nameservers.any? ? "1" : "0",
162
+ "custom_tech_contact" => "0",
163
+ "auto_renew" => "0",
164
+ "contact_set" => contact_set,
165
+ "auth_info" => auth_code,
166
+ }
167
+
168
+ if nameservers.any?
169
+ attrs["nameserver_list"] = nameservers.each_with_index.map { |ns, i|
170
+ { "name" => ns, "sortorder" => (i + 1).to_s }
171
+ }
172
+ end
173
+
174
+ client.call(action: "SW_REGISTER", object: "DOMAIN", attributes: attrs)
175
+ new(name: domain_name, client: client, nameservers: nameservers)
176
+ end
177
+
178
+ private
179
+
180
+ def from_api(domain_name, attrs, client:)
181
+ nameservers = (attrs["nameserver_list"] || []).sort_by { |ns|
182
+ ns["sortorder"].to_i
183
+ }.map { |ns| ns["name"] }
184
+
185
+ contacts = {}
186
+ (attrs["contact_set"] || {}).each do |role, data|
187
+ contacts[role.to_sym] = Contact.from_hash(data) if data.is_a?(Hash)
188
+ end
189
+
190
+ new(
191
+ name: domain_name,
192
+ client: client,
193
+ expires_at: parse_time(attrs["registry_expiredate"] || attrs["expiredate"]),
194
+ created_at: parse_time(attrs["registry_createdate"]),
195
+ updated_at: parse_time(attrs["registry_updatedate"]),
196
+ auto_renew: attrs["auto_renew"].to_s == "1",
197
+ locked: attrs["f_lock_domain"].to_s == "1",
198
+ whois_private: attrs["whois_privacy_state"] == "enabled",
199
+ nameservers: nameservers,
200
+ contacts: contacts
201
+ )
202
+ end
203
+
204
+ def parse_time(str)
205
+ return nil unless str && !str.empty?
206
+ Time.parse(str)
207
+ end
208
+ end
209
+
210
+ def renew!(years: 1)
211
+ current = self.class.find(@name, client: @client)
212
+ expiry_year = current.expires_at&.year || Time.now.year
213
+
214
+ @client.call(action: "RENEW", object: "DOMAIN", attributes: {
215
+ "domain" => @name,
216
+ "auto_renew" => "0",
217
+ "handle" => "process",
218
+ "currentexpirationyear" => expiry_year.to_s,
219
+ "period" => years.to_s,
220
+ })
221
+ end
222
+
223
+ def lock!
224
+ modify_setting("f_lock_domain", "1")
225
+ @locked = true
226
+ end
227
+
228
+ def unlock!
229
+ modify_setting("f_lock_domain", "0")
230
+ @locked = false
231
+ end
232
+
233
+ def set_nameservers(nameservers)
234
+ ns_list = nameservers.each_with_index.map { |ns, i|
235
+ { "name" => ns, "sortorder" => (i + 1).to_s }
236
+ }
237
+ @client.call(action: "MODIFY", object: "DOMAIN",
238
+ attributes: {
239
+ "affect_domains" => "0",
240
+ "data" => "nameserver_list",
241
+ "assign_ns" => nameservers,
242
+ "nameserver_list" => ns_list
243
+ },
244
+ extra: { "domain" => @name })
245
+ @nameservers = nameservers
246
+ end
247
+
248
+ def enable_whois_privacy!
249
+ @client.call(action: "SET", object: "DOMAIN",
250
+ attributes: { "affect_domains" => "0", "data" => "whois_privacy_state", "state" => "enable" },
251
+ extra: { "domain" => @name })
252
+ @whois_private = true
253
+ end
254
+
255
+ def disable_whois_privacy!
256
+ @client.call(action: "SET", object: "DOMAIN",
257
+ attributes: { "affect_domains" => "0", "data" => "whois_privacy_state", "state" => "disable" },
258
+ extra: { "domain" => @name })
259
+ @whois_private = false
260
+ end
261
+
262
+ def set_auto_renew(enabled)
263
+ @client.call(action: "MODIFY", object: "DOMAIN",
264
+ attributes: { "affect_domains" => "0", "data" => "expire_action",
265
+ "auto_renew" => enabled ? "1" : "0", "let_expire" => enabled ? "0" : "1" },
266
+ extra: { "domain" => @name })
267
+ @auto_renew = enabled
268
+ end
269
+
270
+ private
271
+
272
+ def modify_setting(key, value)
273
+ @client.call(action: "MODIFY", object: "DOMAIN",
274
+ attributes: { "affect_domains" => "0", "data" => "status", key => value },
275
+ extra: { "domain" => @name })
276
+ end
277
+ end
278
+ end
@@ -0,0 +1,20 @@
1
+ module OpenSRS
2
+ class Error < StandardError; end
3
+
4
+ class ConnectionError < Error; end
5
+
6
+ class RequestError < Error
7
+ attr_reader :response_code
8
+
9
+ def initialize(message = nil, response_code = nil)
10
+ @response_code = response_code
11
+ super(message)
12
+ end
13
+ end
14
+
15
+ class AuthenticationError < RequestError; end
16
+ class DomainNotFound < RequestError; end
17
+ class DomainUnavailable < RequestError; end
18
+ class TransferError < RequestError; end
19
+ class InsufficientBalance < RequestError; end
20
+ end
data/lib/opensrs.rb ADDED
@@ -0,0 +1,19 @@
1
+ require_relative "opensrs/errors"
2
+ require_relative "opensrs/contact"
3
+ require_relative "opensrs/client"
4
+ require_relative "opensrs/account"
5
+ require_relative "opensrs/domain"
6
+
7
+ module OpenSRS
8
+ class << self
9
+ attr_reader :default_client
10
+
11
+ def configure(username:, api_key:, test: false)
12
+ @default_client = Client.new(username: username, api_key: api_key, test: test)
13
+ end
14
+
15
+ def reset!
16
+ @default_client = nil
17
+ end
18
+ end
19
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: opensrs-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - usiegj00
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: webmock
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ email:
55
+ - 248302+usiegj00@users.noreply.github.com
56
+ executables: []
57
+ extensions: []
58
+ extra_rdoc_files: []
59
+ files:
60
+ - lib/opensrs.rb
61
+ - lib/opensrs/account.rb
62
+ - lib/opensrs/client.rb
63
+ - lib/opensrs/contact.rb
64
+ - lib/opensrs/domain.rb
65
+ - lib/opensrs/errors.rb
66
+ homepage: https://github.com/aluminumio/opensrs-gem
67
+ licenses:
68
+ - MIT
69
+ metadata: {}
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '3.1'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubygems_version: 3.6.9
85
+ specification_version: 4
86
+ summary: Ruby client for the OpenSRS domain registration API
87
+ test_files: []