registrar-client 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.
Files changed (34) hide show
  1. data/LICENSE +19 -0
  2. data/Readme.md +70 -0
  3. data/Spec.md +55 -0
  4. data/examples/enom.example.yml +4 -0
  5. data/examples/purchase_com.rb +7 -0
  6. data/lib/registrar-client.rb +1 -0
  7. data/lib/registrar.rb +14 -0
  8. data/lib/registrar/client.rb +224 -0
  9. data/lib/registrar/contact.rb +31 -0
  10. data/lib/registrar/domain.rb +26 -0
  11. data/lib/registrar/extended_attribute.rb +12 -0
  12. data/lib/registrar/extended_attribute_descriptor.rb +31 -0
  13. data/lib/registrar/extended_attribute_option_descriptor.rb +7 -0
  14. data/lib/registrar/name_server.rb +16 -0
  15. data/lib/registrar/order.rb +43 -0
  16. data/lib/registrar/provider/enom.rb +430 -0
  17. data/lib/registrar/provider/enom/contact.rb +68 -0
  18. data/lib/registrar/provider/enom/extended_attribute.rb +64 -0
  19. data/lib/registrar/provider/enom/extended_attribute_be.rb +0 -0
  20. data/lib/registrar/provider/enom/extended_attribute_ca.rb +40 -0
  21. data/lib/registrar/provider/enom/extended_attribute_io.rb +18 -0
  22. data/lib/registrar/provider/enom/extended_attribute_us.rb +29 -0
  23. data/lib/registrar/provider/enom/order.rb +41 -0
  24. data/lib/registrar/provider/opensrs.rb +133 -0
  25. data/lib/registrar/provider/opensrs/contact.rb +30 -0
  26. data/lib/registrar/provider/opensrs/contact_set.rb +24 -0
  27. data/lib/registrar/provider/opensrs/name_server_list.rb +26 -0
  28. data/lib/registrar/provider/opensrs/operation.rb +42 -0
  29. data/lib/registrar/provider/opensrs/order.rb +59 -0
  30. data/lib/registrar/provider/opensrs/tld_data.rb +29 -0
  31. data/lib/registrar/provider/opensrs/tld_data_us.rb +48 -0
  32. data/lib/registrar/purchase_options.rb +29 -0
  33. data/lib/registrar/renewal_options.rb +8 -0
  34. metadata +177 -0
@@ -0,0 +1,12 @@
1
+ module Registrar
2
+ class ExtendedAttribute
3
+ attr_accessor :tld
4
+ attr_accessor :name
5
+ attr_accessor :value
6
+ def initialize(tld, name, value)
7
+ @tld = tld
8
+ @name = name
9
+ @value = value
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,31 @@
1
+ module Registrar
2
+ class ExtendedAttributeDescriptor
3
+ attr_accessor :name
4
+ attr_accessor :description
5
+ attr_accessor :required
6
+ attr_accessor :child
7
+ attr_accessor :application
8
+ attr_accessor :user_defined
9
+ attr_accessor :options
10
+ attr_accessor :apply_to_registrar
11
+
12
+ alias :required? :required
13
+ alias :child? :child
14
+ alias :user_defined? :user_defined
15
+ alias :apply_to_registrar? :apply_to_registrar
16
+
17
+ def initialize
18
+ @options = []
19
+ end
20
+
21
+ def serializable_hash
22
+ {
23
+ 'name' => name,
24
+ 'description' => description,
25
+ 'required' => required,
26
+ 'options' => options
27
+ }
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ module Registrar
2
+ class ExtendedAttributeOptionDescriptor
3
+ attr_accessor :title
4
+ attr_accessor :value
5
+ attr_accessor :description
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ module Registrar
2
+ class NameServer
3
+ include Comparable
4
+
5
+ attr_reader :name
6
+ attr_accessor :ip_address
7
+
8
+ def initialize(name)
9
+ @name = name
10
+ end
11
+
12
+ def <=> other
13
+ self.name <=> other.name
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,43 @@
1
+ module Registrar #:nodoc:
2
+ # Instances of this class contain details about the current state of a
3
+ # particular order with the registrar.
4
+ class Order
5
+ # The service-specific identifier for the order.
6
+ attr_reader :identifier
7
+
8
+ # The current status of the order
9
+ attr_accessor :status
10
+
11
+ # The date the order was created
12
+ attr_accessor :date
13
+
14
+ # Construct a new Order instance.
15
+ def initialize(identifier)
16
+ @identifier = identifier
17
+ @status = :unknown
18
+ @successful = false
19
+ end
20
+
21
+ # Get the domains associated with this order
22
+ def domains
23
+ @domains ||= []
24
+ end
25
+
26
+ # Return true if the order is complete.
27
+ def complete?
28
+ ![:open, :unknown].include?(@status)
29
+ end
30
+
31
+ def open?
32
+ @status == :open
33
+ end
34
+
35
+ def successful?
36
+ @successful
37
+ end
38
+
39
+ def successful=(successful)
40
+ @successful = successful
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,430 @@
1
+ require 'httparty'
2
+ require 'tzinfo'
3
+
4
+ require 'registrar/provider/enom/contact'
5
+ require 'registrar/provider/enom/extended_attribute'
6
+ require 'registrar/provider/enom/order'
7
+
8
+ module Registrar
9
+ module Provider
10
+ # Implementation of a registrar provider for Enom (http://www.enom.com/).
11
+ class Enom
12
+ include HTTParty
13
+
14
+ attr_accessor :url, :username, :password
15
+
16
+ def initialize(url, username, password)
17
+ @url = url
18
+ @username = username
19
+ @password = password
20
+ end
21
+
22
+ def parse(name)
23
+ query = base_query.merge('Command' => 'ParseDomain')
24
+ response = execute(query.merge('PassedDomain' => name))
25
+
26
+ [response['ParseDomain']['SLD'], response['ParseDomain']['TLD']]
27
+ end
28
+
29
+ def available?(name)
30
+ sld, tld = parse(name)
31
+
32
+ query = base_query.merge('Command' => 'Check')
33
+ response = execute(query.merge('SLD' => sld, 'TLD' => tld))
34
+
35
+ response['RRPCode'] == '210'
36
+ end
37
+
38
+ def find(name)
39
+ sld, tld = parse(name)
40
+ query = base_query.merge('Command' => 'GetDomainInfo')
41
+
42
+ response = execute(query.merge('SLD' => sld, 'TLD' => tld))
43
+
44
+ domain = Registrar::Domain.new(name)
45
+ domain.expiration = response['GetDomainInfo']['status']['expiration']
46
+ domain.registration_status = response['GetDomainInfo']['status']['registrationstatus']
47
+ domain.order = order_for_domain(name)
48
+ domain
49
+ end
50
+
51
+ def purchase(name, registrant, purchase_options=nil)
52
+ purchase_options ||= Registrar::PurchaseOptions.new
53
+
54
+ sld, tld = parse(name)
55
+ query = base_query.merge('Command' => 'Purchase', 'SLD' => sld, 'TLD' => tld)
56
+ registrant = Enom::Contact.new(registrant)
57
+
58
+ if registrant
59
+ query.merge!(registrant.to_query("Registrant"))
60
+ query.merge!(registrant.to_query("AuxBilling"))
61
+ query.merge!(registrant.to_query("Tech"))
62
+ query.merge!(registrant.to_query("Admin"))
63
+ end
64
+
65
+ if purchase_options.has_name_servers?
66
+ query['IgnoreNSFail'] = 'Yes'
67
+ purchase_options.name_servers.each_with_index do |name_server, i|
68
+ case name_server
69
+ when String
70
+ query["NS#{i+1}"] = name_server
71
+ else
72
+ query["NS#{i+1}"] = name_server.name
73
+ end
74
+
75
+ end
76
+ else
77
+ query['UseDNS'] = 'default'
78
+ end
79
+
80
+ if purchase_options.has_extended_attributes?
81
+ extended_attributes = purchase_options.extended_attributes.map { |a| Enom::ExtendedAttribute.new(a) }
82
+ extended_attributes.each do |extended_attribute|
83
+ query[extended_attribute.name] = extended_attribute.value
84
+ end
85
+ end
86
+
87
+ query['NumYears'] = purchase_options.number_of_years || minimum_number_of_years(tld)
88
+ query['IDNCode'] = purchase_options.language if purchase_options.language
89
+
90
+ response = execute(query)
91
+
92
+ registrant.identifier = response['RegistrantPartyID']
93
+
94
+ domain = Registrar::Domain.new(name)
95
+ domain.registrant = registrant
96
+ domain.lockable = response['IsLockable'].downcase == 'true'
97
+ domain.real_time = response['IsRealTimeTLD'].downcase == 'true'
98
+ order = order(response['OrderID'])
99
+ order.domains << domain
100
+ domain.order = order
101
+ order
102
+ end
103
+
104
+ def renew(name, renewal_options=nil)
105
+ renewal_options ||= Registrar::RenewalOptions.new
106
+ sld, tld = parse(name)
107
+ query = base_query.merge('Command' => 'Extend', 'SLD' => sld, 'TLD' => tld)
108
+ query = query.merge('NumYears' => renewal_options.number_of_years)
109
+ response = execute(query)
110
+ response['Extension'] && response['Extension'].downcase == 'successful'
111
+ end
112
+
113
+ def auto_renew?(name)
114
+ sld, tld = parse(name)
115
+ query = base_query.merge('Command' => 'GetRenew', 'TLD' => tld, 'SLD' => sld)
116
+ response = execute_command(query)
117
+ response['auto_renew'] == '1'
118
+ end
119
+
120
+ def enable_auto_renewal(name)
121
+ sld, tld = parse(name)
122
+ query = base_query.merge('Command' => 'SetRenew', 'TLD' => tld, 'SLD' => sld)
123
+ query = query.merge('RenewFlag' => '1')
124
+ response = execute_command(query)
125
+ response['RenewName'] == 'True'
126
+ end
127
+
128
+ def disable_auto_renewal(name)
129
+ sld, tld = parse(name)
130
+ query = base_query.merge('Command' => 'SetRenew', 'TLD' => tld, 'SLD' => sld)
131
+ query = query.merge('RenewFlag' => '0')
132
+ response = execute_command(query)
133
+ response['RenewName'] == 'False'
134
+ end
135
+
136
+ def order(id)
137
+ query = base_query.merge('Command' => 'GetOrderDetail', 'OrderID' => id.to_s)
138
+ response = execute(query)
139
+
140
+ order = Enom::Order.new(response['Order']['OrderID'])
141
+ order.order_date = response['Order']['OrderDate']
142
+ order.order_status = response['Order']['OrderDetail']['OrderStatus']
143
+ order.status = response['Order']['OrderDetail']['Status']
144
+ order.to_order
145
+ end
146
+
147
+ def order_for_domain(name)
148
+ sld, tld = parse(name)
149
+ query = base_query.merge('Command' => 'GetDomainStatus', 'SLD' => sld, 'TLD' => tld)
150
+ response = execute_command(query)
151
+ order(response['DomainStatus']['OrderID'])
152
+ end
153
+
154
+ def name_servers(name)
155
+ sld, tld = parse(name)
156
+ query = base_query.merge('Command' => 'GetDNS', 'TLD' => tld, 'SLD' => sld)
157
+ response = execute_command(query)
158
+ [response['dns']].flatten
159
+ end
160
+ alias :nameservers :name_servers
161
+
162
+ def set_name_servers(name, name_servers=[])
163
+ sld, tld = parse(name)
164
+ query = base_query.merge('Command' => 'ModifyNS', 'TLD' => tld, 'SLD' => sld)
165
+
166
+ name_server_hash = {}
167
+ if name_servers.length == 0
168
+ name_server_hash["NS1"] = ""
169
+ else
170
+ name_servers.each_with_index do |ns_name, index|
171
+ name_server_hash["NS#{index + 1}"] = ns_name
172
+ end
173
+ end
174
+ query = query.merge(name_server_hash)
175
+
176
+ response = execute_command(query)
177
+
178
+ name_servers
179
+ end
180
+
181
+ def find_name_server(name)
182
+ query = base_query.merge('Command' => 'CheckNSStatus', 'CheckNSName' => name)
183
+ response = execute_command(query)
184
+
185
+ if response['NsCheckSuccess'] == '1'
186
+ name_server = Registrar::NameServer.new(response['CheckNsStatus']['name'])
187
+ name_server.ip_address = response['CheckNsStatus']['ipaddress']
188
+ name_server
189
+ else
190
+ raise RuntimeError, "Name server not found for #{name}"
191
+ end
192
+ end
193
+
194
+ def register_name_server(name_server)
195
+ query = base_query.merge('Command' => 'RegisterNameServer', 'Add' => 'true', 'NSName' => name_server.name, 'IP' => name_server.ip_address)
196
+ response = execute_command(query)
197
+
198
+ if response['RRPCode'] == '200'
199
+ name_server = Registrar::NameServer.new(response['RegisterNameserver']['NS'])
200
+ name_server.ip_address = response['RegisterNameserver']['IP']
201
+ name_server
202
+ else
203
+ raise RuntimeError, "Unable to create name server: #{response['RRPText']}"
204
+ end
205
+ end
206
+
207
+ def extended_attributes(name)
208
+ sld, tld = parse(name)
209
+ query = base_query.merge('Command' => 'GetExtAttributes', 'TLD' => tld)
210
+ response = execute(query)
211
+ return nil unless response['Attributes']
212
+ [response['Attributes']['Attribute']].flatten.map do |enom_attribute|
213
+ extended_attribute = Registrar::ExtendedAttributeDescriptor.new
214
+ extended_attribute.name = enom_attribute['Name']
215
+ extended_attribute.description = enom_attribute['Description']
216
+ extended_attribute.child = enom_attribute['IsChild'] == '1'
217
+ extended_attribute.required = enom_attribute['Required'] == '1'
218
+ extended_attribute.application = enom_attribute['Application']
219
+ extended_attribute.user_defined = enom_attribute['UserDefined'] == 'True'
220
+ extended_attribute.apply_to_registrar = enom_attribute['Application'] == '2'
221
+
222
+ if enom_attribute['Options']
223
+ extended_attribute.options = [enom_attribute['Options']['Option']].flatten.map do |enom_option|
224
+ option = Registrar::ExtendedAttributeOptionDescriptor.new
225
+ option.title = enom_option['Title']
226
+ option.value = enom_option['Value']
227
+ option.description = enom_option['Description']
228
+ option
229
+ end
230
+ end
231
+
232
+ extended_attribute
233
+ end
234
+ end
235
+
236
+ def minimum_number_of_years(tld)
237
+ {
238
+ 'co.uk' => 2,
239
+ 'org.uk' => 2,
240
+ 'nu' => 2,
241
+ 'tm' => 10,
242
+ 'com.mx' => 2,
243
+ 'me.uk' => 2
244
+ }[tld] || 1
245
+ end
246
+
247
+ def tld_retail_transfer_price(tld)
248
+ Enom::PricingEngine.tld_retail_transfer_price(tld)
249
+ end
250
+
251
+ # Get a Hash of all of the contacts for the domain. The Hash will have the following
252
+ # key/value pairs:
253
+ #
254
+ # * :registrant => The domain registrant
255
+ # * :aux_billing => An customer specified billing contact
256
+ # * :tech => The technical contact for the domain
257
+ # * :admin => The administrative contact for the domain
258
+ # * :billing => The Enom billing contact (DNSimple)
259
+ def contacts(domain)
260
+ sld, tld = parse(domain.name)
261
+
262
+ query = base_query.merge('Command' => 'GetContacts', 'TLD' => tld, 'SLD' => sld)
263
+
264
+ response = execute(query)
265
+
266
+ contacts = {}
267
+ registrant_hash = response['GetContacts']['Registrant']
268
+ contacts[:registrant] = Enom::Contact.from_response('Registrant', registrant_hash)
269
+ aux_billing_hash = response['GetContacts']['AuxBilling']
270
+ contacts[:aux_billing] = Enom::Contact.from_response('AuxBilling', aux_billing_hash)
271
+
272
+ tech_hash = response['GetContacts']['Tech']
273
+ contacts[:tech] = Enom::Contact.from_response('Tech', tech_hash)
274
+
275
+ admin_hash = response['GetContacts']['Admin']
276
+ contacts[:admin] = Enom::Contact.from_response('Admin', admin_hash)
277
+
278
+ billing_hash = response['GetContacts']['Billing']
279
+ contacts[:billing] = Enom::Contact.from_response('Billing', billing_hash)
280
+
281
+ contacts
282
+ end
283
+
284
+ # Update the registrant information for a domain. For some TLDs this will include
285
+ # providing extended attributes. If the TLD does not require extended attributes
286
+ # then send nil or an empty Hash for the extended_attributes argument.
287
+ def update_registrant(domain, registrant, extended_attributes=nil)
288
+ registrant = Enom::Contact.new(registrant)
289
+
290
+ sld, tld = parse(domain.name)
291
+ query = base_query.merge(
292
+ 'Command' => 'Contacts',
293
+ 'TLD' => tld,
294
+ 'SLD' => sld
295
+ )
296
+
297
+ query = query.merge('ContactType' => 'Registrant')
298
+ query = query.merge(registrant.to_query('Registrant'))
299
+
300
+ if extended_attributes
301
+ extended_attributes.each do |name, value|
302
+ query[name] = value
303
+ end
304
+ end
305
+
306
+ response = execute_command(query) # TODO: should something else be returned here?
307
+ end
308
+
309
+ # Update the tech, aux billing and administrative contacts for the domain. Right
310
+ # now the same contact must be used for all of these contact types.
311
+ def update_contacts(domain, contact)
312
+ contact = Enom::Contact.new(contact)
313
+
314
+ sld, tld = parse(domain.name)
315
+ query = base_query.merge(
316
+ 'Command' => 'Contacts',
317
+ 'TLD' => tld,
318
+ 'SLD' => sld
319
+ )
320
+
321
+ tech_contact_query = query.merge('ContactType' => 'Tech')
322
+ tech_contact_query = tech_contact_query.merge(contact.to_query('Tech'))
323
+ response = execute_command(tech_contact_query)
324
+
325
+ admin_contact_query = query.merge('ContactType' => 'Admin')
326
+ admin_contact_query = admin_contact_query.merge(contact.to_query('Admin'))
327
+ response = execute_command(admin_contact_query)
328
+
329
+ billing_contact_query = query.merge('ContactType' => 'AuxBilling')
330
+ billing_contact_query = billing_contact_query.merge(contact.to_query('AuxBilling'))
331
+ response = execute(billing_contact_query)
332
+
333
+ contacts(domain)
334
+ end
335
+
336
+ # Update the tech contact for the domain.
337
+ def update_technical_contact(domain, contact)
338
+ contact = Enom::Contact.new(contact)
339
+
340
+ sld, tld = parse(domain.name)
341
+ query = base_query.merge(
342
+ 'Command' => 'Contacts',
343
+ 'TLD' => tld,
344
+ 'SLD' => sld
345
+ )
346
+
347
+ query = query.merge('ContactType' => 'Tech')
348
+ query = query.merge(contact.to_query('Tech'))
349
+ response = execute(query)
350
+ contacts(domain)[:tech]
351
+ end
352
+
353
+ # Update the admin contact for the domain.
354
+ def update_administrative_contact(domain, contact)
355
+ contact = Enom::Contact.new(contact)
356
+
357
+ sld, tld = parse(domain.name)
358
+ query = base_query.merge(
359
+ 'Command' => 'Contacts',
360
+ 'TLD' => tld,
361
+ 'SLD' => sld
362
+ )
363
+
364
+ query = query.merge('ContactType' => 'Admin')
365
+ query = query.merge(contact.to_query('Admin'))
366
+ response = execute(query)
367
+ contacts(domain)[:admin]
368
+ end
369
+
370
+ # Update the aux billing contact for the domain.
371
+ def update_aux_billing_contact(domain, contact)
372
+ contact = Enom::Contact.new(contact)
373
+
374
+ sld, tld = parse(domain.name)
375
+ query = base_query.merge(
376
+ 'Command' => 'Contacts',
377
+ 'TLD' => tld,
378
+ 'SLD' => sld
379
+ )
380
+
381
+ query = query.merge('ContactType' => 'AuxBilling')
382
+ query = query.merge(contact.to_query('AuxBilling'))
383
+ response = execute(query)
384
+ contacts(domain)[:aux_billing]
385
+ end
386
+
387
+ private
388
+ def execute(query)
389
+ Encoding.default_internal = Encoding.default_external = "UTF-8"
390
+ options = {:query => query, :parser => EnomParser}
391
+ response = self.class.get(url, options)['interface_response']
392
+ raise Registrar::RegistrarError.new("Response from Enom was nil") if response.nil?
393
+ raise EnomError.new(response) if response['ErrCount'] != '0'
394
+ response
395
+ end
396
+ alias :execute_command :execute
397
+
398
+ def base_query
399
+ {
400
+ 'UID' => username,
401
+ 'PW' => password,
402
+ 'ResponseType' => 'XML'
403
+ }
404
+ end
405
+
406
+ end
407
+
408
+ class EnomError < Registrar::RegistrarError
409
+ attr_reader :response
410
+ attr_reader :errors
411
+
412
+ def initialize(response)
413
+ @response = response
414
+ @errors = []
415
+
416
+ response['errors'].each do |k, err|
417
+ @errors << err
418
+ end
419
+
420
+ super response['errors'].values.join(", ")
421
+ end
422
+ end
423
+
424
+ class EnomParser < HTTParty::Parser
425
+ def body
426
+ @body.force_encoding('UTF-8')
427
+ end
428
+ end
429
+ end
430
+ end